Merge branch 'master' of github.com:mongodb/mongo-python-driver
This commit is contained in:
commit
a665877986
@ -365,7 +365,7 @@ functions:
|
||||
${PREPARE_SHELL}
|
||||
set -o xtrace
|
||||
export PYTHON_BINARY=${PYTHON_BINARY}
|
||||
bash ${PROJECT_DIRECTORY}/.evergreen/tox.sh -m test-mockupdb
|
||||
bash ${PROJECT_DIRECTORY}/.evergreen/hatch.sh test:test-mockupdb
|
||||
|
||||
"run doctests":
|
||||
- command: shell.exec
|
||||
@ -375,18 +375,19 @@ functions:
|
||||
script: |
|
||||
${PREPARE_SHELL}
|
||||
set -o xtrace
|
||||
PYTHON_BINARY=${PYTHON_BINARY} bash ${PROJECT_DIRECTORY}/.evergreen/tox.sh -m doc-test
|
||||
PYTHON_BINARY=${PYTHON_BINARY} bash ${PROJECT_DIRECTORY}/.evergreen/hatch.sh doctest:test
|
||||
|
||||
"run tests":
|
||||
- command: shell.exec
|
||||
params:
|
||||
working_dir: "src"
|
||||
shell: bash
|
||||
include_expansions_in_env: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"]
|
||||
background: true
|
||||
include_expansions_in_env: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"]
|
||||
script: |
|
||||
${PREPARE_SHELL}
|
||||
if [ -n "${test_encryption}" ]; then
|
||||
. .evergreen/tox.sh -m setup-encryption
|
||||
./.evergreen/hatch.sh encryption:setup
|
||||
fi
|
||||
- command: shell.exec
|
||||
type: test
|
||||
@ -407,6 +408,7 @@ functions:
|
||||
if [ -n "${test_encryption}" ]; then
|
||||
# Disable xtrace (just in case it was accidentally set).
|
||||
set +x
|
||||
bash ${DRIVERS_TOOLS}/.evergreen/csfle/await-servers.sh
|
||||
export TEST_ENCRYPTION=1
|
||||
if [ -n "${test_encryption_pyopenssl}" ]; then
|
||||
export TEST_ENCRYPTION_PYOPENSSL=1
|
||||
@ -446,7 +448,7 @@ functions:
|
||||
SSL=${SSL} \
|
||||
TEST_DATA_LAKE=${TEST_DATA_LAKE} \
|
||||
MONGODB_API_VERSION=${MONGODB_API_VERSION} \
|
||||
bash ${PROJECT_DIRECTORY}/.evergreen/tox.sh -m test-eg
|
||||
bash ${PROJECT_DIRECTORY}/.evergreen/hatch.sh test:test-eg
|
||||
|
||||
"run enterprise auth tests":
|
||||
- command: shell.exec
|
||||
@ -462,7 +464,7 @@ functions:
|
||||
PYTHON_BINARY="${PYTHON_BINARY}" \
|
||||
TEST_ENTERPRISE_AUTH=1 \
|
||||
AUTH=auth \
|
||||
bash ${PROJECT_DIRECTORY}/.evergreen/tox.sh -m test-eg
|
||||
bash ${PROJECT_DIRECTORY}/.evergreen/hatch.sh test:test-eg
|
||||
|
||||
"run atlas tests":
|
||||
- command: shell.exec
|
||||
@ -478,7 +480,7 @@ functions:
|
||||
PROJECT_DIRECTORY="${PROJECT_DIRECTORY}" \
|
||||
PYTHON_BINARY="${PYTHON_BINARY}" \
|
||||
TEST_ATLAS=1 \
|
||||
bash ${PROJECT_DIRECTORY}/.evergreen/tox.sh -m test-eg
|
||||
bash ${PROJECT_DIRECTORY}/.evergreen/hatch.sh test:test-eg
|
||||
|
||||
"get aws auth secrets":
|
||||
- command: subprocess.exec
|
||||
@ -597,7 +599,7 @@ functions:
|
||||
working_dir: "src"
|
||||
script: |
|
||||
${PREPARE_SHELL}
|
||||
. .evergreen/tox.sh -m teardown-encryption
|
||||
. .evergreen/hatch.sh encryption:teardown
|
||||
rm -rf $DRIVERS_TOOLS || true
|
||||
rm -f ./secrets-export.sh || true
|
||||
|
||||
@ -683,7 +685,7 @@ functions:
|
||||
PYTHON_BINARY=${PYTHON_BINARY} \
|
||||
CA_FILE="$DRIVERS_TOOLS/.evergreen/ocsp/${OCSP_ALGORITHM}/ca.pem" \
|
||||
OCSP_TLS_SHOULD_SUCCEED="${OCSP_TLS_SHOULD_SUCCEED}" \
|
||||
bash ${PROJECT_DIRECTORY}/.evergreen/tox.sh -m test-eg
|
||||
bash ${PROJECT_DIRECTORY}/.evergreen/hatch.sh test:test-eg
|
||||
|
||||
run-valid-ocsp-server:
|
||||
- command: shell.exec
|
||||
@ -2128,7 +2130,7 @@ tasks:
|
||||
${PREPARE_SHELL}
|
||||
export PYTHON_BINARY=/opt/mongodbtoolchain/v4/bin/python3
|
||||
export LIBMONGOCRYPT_URL=https://s3.amazonaws.com/${bucket_name}/libmongocrypt/debian10/master/latest/libmongocrypt.tar.gz
|
||||
SUCCESS=false TEST_FLE_GCP_AUTO=1 ./.evergreen/tox.sh -m test-eg
|
||||
SUCCESS=false TEST_FLE_GCP_AUTO=1 ./.evergreen/hatch.sh test:test-eg
|
||||
|
||||
- name: testazurekms-task
|
||||
commands:
|
||||
|
||||
24
.evergreen/hatch.sh
Normal file
24
.evergreen/hatch.sh
Normal file
@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
set -o errexit # Exit the script with error if any of the commands fail
|
||||
set -x
|
||||
|
||||
. .evergreen/utils.sh
|
||||
|
||||
if [ -z "$PYTHON_BINARY" ]; then
|
||||
PYTHON_BINARY=$(find_python3)
|
||||
fi
|
||||
|
||||
if $PYTHON_BINARY -m hatch --version; then
|
||||
run_hatch() {
|
||||
$PYTHON_BINARY hatch run "$@"
|
||||
}
|
||||
else # No toolchain hatch present, set up virtualenv before installing hatch
|
||||
createvirtualenv "$PYTHON_BINARY" hatchenv
|
||||
trap "deactivate; rm -rf hatchenv" EXIT HUP
|
||||
python -m pip install -q hatch
|
||||
run_hatch() {
|
||||
python -m hatch run "$@"
|
||||
}
|
||||
fi
|
||||
|
||||
run_hatch "${@:1}"
|
||||
@ -7,4 +7,4 @@ PYTHON_BINARY=/opt/mongodbtoolchain/v4/bin/python3 \
|
||||
KEY_VAULT_ENDPOINT="${AZUREKMS_KEYVAULTENDPOINT}" \
|
||||
LIBMONGOCRYPT_URL=https://s3.amazonaws.com/mciuploads/libmongocrypt/debian10/master/latest/libmongocrypt.tar.gz \
|
||||
SUCCESS=false TEST_FLE_AZURE_AUTO=1 \
|
||||
./.evergreen/tox.sh -m test-eg
|
||||
./.evergreen/hatch.sh test:test-eg
|
||||
|
||||
@ -16,6 +16,6 @@ AZUREKMS_CMD="tar xf mongo-python-driver.tgz" \
|
||||
$DRIVERS_TOOLS/.evergreen/csfle/azurekms/run-command.sh
|
||||
echo "Untarring file ... end"
|
||||
echo "Running test ... begin"
|
||||
AZUREKMS_CMD="KEY_NAME=\"$AZUREKMS_KEYNAME\" KEY_VAULT_ENDPOINT=\"$AZUREKMS_KEYVAULTENDPOINT\" LIBMONGOCRYPT_URL=https://s3.amazonaws.com/mciuploads/libmongocrypt/debian10/master/latest/libmongocrypt.tar.gz SUCCESS=true TEST_FLE_AZURE_AUTO=1 ./.evergreen/tox.sh -m test-eg" \
|
||||
AZUREKMS_CMD="KEY_NAME=\"$AZUREKMS_KEYNAME\" KEY_VAULT_ENDPOINT=\"$AZUREKMS_KEYVAULTENDPOINT\" LIBMONGOCRYPT_URL=https://s3.amazonaws.com/mciuploads/libmongocrypt/debian10/master/latest/libmongocrypt.tar.gz SUCCESS=true TEST_FLE_AZURE_AUTO=1 ./.evergreen/hatch.sh test:test-eg" \
|
||||
$DRIVERS_TOOLS/.evergreen/csfle/azurekms/run-command.sh
|
||||
echo "Running test ... end"
|
||||
|
||||
@ -14,5 +14,5 @@ echo "Untarring file ... begin"
|
||||
GCPKMS_CMD="tar xf mongo-python-driver.tgz" $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/run-command.sh
|
||||
echo "Untarring file ... end"
|
||||
echo "Running test ... begin"
|
||||
GCPKMS_CMD="SUCCESS=true TEST_FLE_GCP_AUTO=1 LIBMONGOCRYPT_URL=https://s3.amazonaws.com/mciuploads/libmongocrypt/debian10/master/latest/libmongocrypt.tar.gz ./.evergreen/tox.sh -m test-eg" $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/run-command.sh
|
||||
GCPKMS_CMD="SUCCESS=true TEST_FLE_GCP_AUTO=1 LIBMONGOCRYPT_URL=https://s3.amazonaws.com/mciuploads/libmongocrypt/debian10/master/latest/libmongocrypt.tar.gz ./.evergreen/hatch.sh test:test-eg" $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/run-command.sh
|
||||
echo "Running test ... end"
|
||||
|
||||
@ -30,5 +30,5 @@ export TEST_AUTH_AWS=1
|
||||
export AUTH="auth"
|
||||
export SET_XTRACE_ON=1
|
||||
cd src
|
||||
$PYTHON_BINARY -m pip install -q --user tox
|
||||
bash .evergreen/tox.sh -m test-eg
|
||||
$PYTHON_BINARY -m pip install -q --user hatch
|
||||
bash .evergreen/hatch.sh test:test-eg
|
||||
|
||||
@ -24,4 +24,4 @@ set -x
|
||||
export TEST_AUTH_AWS=1
|
||||
export AUTH="auth"
|
||||
export SET_XTRACE_ON=1
|
||||
bash ./.evergreen/tox.sh -m test-eg
|
||||
bash ./.evergreen/hatch.sh test:test-eg
|
||||
|
||||
@ -29,4 +29,4 @@ fi
|
||||
export TEST_AUTH_OIDC=1
|
||||
export COVERAGE=1
|
||||
export AUTH="auth"
|
||||
bash ./.evergreen/tox.sh -m test-eg -- "${@:1}"
|
||||
bash ./.evergreen/hatch.sh test:test-eg -- "${@:1}"
|
||||
|
||||
@ -16,4 +16,4 @@ export OUTPUT_FILE="${PROJECT_DIRECTORY}/results.json"
|
||||
export PYTHON_BINARY=/opt/mongodbtoolchain/v4/bin/python3
|
||||
export PERF_TEST=1
|
||||
|
||||
bash ./.evergreen/tox.sh -m test-eg
|
||||
bash ./.evergreen/hatch.sh test:test-eg
|
||||
|
||||
@ -123,9 +123,9 @@ if [ -n "$TEST_ENCRYPTION" ] || [ -n "$TEST_FLE_AZURE_AUTO" ] || [ -n "$TEST_FLE
|
||||
|
||||
python -m pip install '.[encryption]'
|
||||
|
||||
# Install libmongocrypt if necessary.
|
||||
# Setup encryption if necessary.
|
||||
if [ ! -d "libmongocrypt" ]; then
|
||||
bash ./.evergreen/setup-libmongocrypt.sh
|
||||
bash ./.evergreen/setup-encryption.sh
|
||||
fi
|
||||
|
||||
# Use the nocrypto build to avoid dependency issues with older windows/python versions.
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
set -o errexit # Exit the script with error if any of the commands fail
|
||||
set -o xtrace
|
||||
|
||||
if [ -z "${DRIVERS_TOOLS}" ]; then
|
||||
echo "Missing environment variable DRIVERS_TOOLS"
|
||||
fi
|
||||
|
||||
TARGET=""
|
||||
|
||||
if [ "Windows_NT" = "${OS:-''}" ]; then # Magic variable in cygwin
|
||||
@ -45,3 +49,6 @@ mkdir libmongocrypt
|
||||
tar xzf libmongocrypt.tar.gz -C ./libmongocrypt
|
||||
ls -la libmongocrypt
|
||||
ls -la libmongocrypt/nocrypto
|
||||
|
||||
bash ${DRIVERS_TOOLS}/.evergreen/csfle/setup-secrets.sh
|
||||
bash ${DRIVERS_TOOLS}/.evergreen/csfle/start-servers.sh
|
||||
10
.evergreen/teardown-encryption.sh
Normal file
10
.evergreen/teardown-encryption.sh
Normal file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -o errexit # Exit the script with error if any of the commands fail
|
||||
set -o xtrace
|
||||
|
||||
if [ -z "${DRIVERS_TOOLS}" ]; then
|
||||
echo "Missing environment variable DRIVERS_TOOLS"
|
||||
fi
|
||||
|
||||
bash ${DRIVERS_TOOLS}/.evergreen/csfle/stop-servers.sh
|
||||
rm -rf libmongocrypt/ libmongocrypt_git/ libmongocrypt.tar.gz mongocryptd.pid
|
||||
@ -1,24 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -o errexit # Exit the script with error if any of the commands fail
|
||||
set -x
|
||||
|
||||
. .evergreen/utils.sh
|
||||
|
||||
if [ -z "$PYTHON_BINARY" ]; then
|
||||
PYTHON_BINARY=$(find_python3)
|
||||
fi
|
||||
|
||||
if $PYTHON_BINARY -m tox --version; then
|
||||
run_tox() {
|
||||
$PYTHON_BINARY -m tox "$@"
|
||||
}
|
||||
else # No toolchain present, set up virtualenv before installing tox
|
||||
createvirtualenv "$PYTHON_BINARY" toxenv
|
||||
trap "deactivate; rm -rf toxenv" EXIT HUP
|
||||
python -m pip install -q tox
|
||||
run_tox() {
|
||||
python -m tox "$@"
|
||||
}
|
||||
fi
|
||||
|
||||
run_tox "${@:1}"
|
||||
@ -66,7 +66,7 @@ createvirtualenv () {
|
||||
|
||||
export PIP_QUIET=1
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --upgrade tox
|
||||
python -m pip install --upgrade hatch
|
||||
}
|
||||
|
||||
# Usage:
|
||||
|
||||
39
.github/workflows/test-python.yml
vendored
39
.github/workflows/test-python.yml
vendored
@ -27,10 +27,10 @@ jobs:
|
||||
cache-dependency-path: 'pyproject.toml'
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install -U pip tox
|
||||
python -m pip install -U pip hatch
|
||||
- name: Run linters
|
||||
run: |
|
||||
tox -m lint-manual
|
||||
hatch run lint:run-manual
|
||||
- name: Run compilation
|
||||
run: |
|
||||
export PYMONGO_C_EXT_MUST_BUILD=1
|
||||
@ -38,7 +38,7 @@ jobs:
|
||||
python tools/fail_if_no_c.py
|
||||
- name: Run typecheck
|
||||
run: |
|
||||
tox -m typecheck
|
||||
hatch run typing:check
|
||||
- run: |
|
||||
sudo apt-get install -y cppcheck
|
||||
- run: |
|
||||
@ -51,7 +51,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-20.04]
|
||||
python-version: ["3.8", "3.11", "pypy-3.9"]
|
||||
python-version: ["3.8", "pypy-3.9", "3.13"]
|
||||
name: CPython ${{ matrix.python-version }}-${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -61,19 +61,26 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'pyproject.toml'
|
||||
allow-prereleases: true
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -q tox
|
||||
pip install -U pip
|
||||
if [ "${{ matrix.python-version }}" == "3.13" ]; then
|
||||
pip install --pre cffi setuptools
|
||||
pip install --no-build-isolation hatch
|
||||
else
|
||||
pip install hatch
|
||||
fi
|
||||
- name: Start MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.10.0
|
||||
with:
|
||||
mongodb-version: 4.4
|
||||
mongodb-version: 6.0
|
||||
- name: Run tests
|
||||
run: |
|
||||
tox -m test
|
||||
hatch run test:test
|
||||
- name: Run async tests
|
||||
run: |
|
||||
tox -m test-async
|
||||
hatch run test:test-async
|
||||
|
||||
doctest:
|
||||
runs-on: ubuntu-latest
|
||||
@ -88,14 +95,14 @@ jobs:
|
||||
cache-dependency-path: 'pyproject.toml'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -q tox
|
||||
pip install -U hatch pip
|
||||
- name: Start MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.10.0
|
||||
with:
|
||||
mongodb-version: 4.4
|
||||
- name: Run tests
|
||||
run: |
|
||||
tox -m doc-test
|
||||
hatch run doctest:test
|
||||
|
||||
docs:
|
||||
name: Docs Checks
|
||||
@ -110,10 +117,10 @@ jobs:
|
||||
python-version: '3.8'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -q tox
|
||||
pip install -U pip hatch
|
||||
- name: Build docs
|
||||
run: |
|
||||
tox -m doc
|
||||
hatch run doc:build
|
||||
|
||||
linkcheck:
|
||||
name: Link Check
|
||||
@ -128,10 +135,10 @@ jobs:
|
||||
python-version: '3.8'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -q tox
|
||||
pip install -U pip hatch
|
||||
- name: Build docs
|
||||
run: |
|
||||
tox -m linkcheck
|
||||
hatch run doc:linkcheck
|
||||
|
||||
typing:
|
||||
name: Typing Tests
|
||||
@ -148,10 +155,10 @@ jobs:
|
||||
cache-dependency-path: 'pyproject.toml'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -q tox
|
||||
pip install -U pip hatch
|
||||
- name: Run typecheck
|
||||
run: |
|
||||
tox -m typecheck
|
||||
hatch run typing:check
|
||||
|
||||
make_sdist:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@ -28,8 +28,9 @@ including 4 space indents and 79 character line limits.
|
||||
|
||||
- Avoid backward breaking changes if at all possible.
|
||||
- Write inline documentation for new classes and methods.
|
||||
- We use [hatch](https://hatch.pypa.io/dev/) for our script runner and packaging tool.
|
||||
- Write tests and make sure they pass (make sure you have a mongod
|
||||
running on the default port, then execute `tox -e test` from the cmd
|
||||
running on the default port, then execute `hatch run test:test` from the cmd
|
||||
line to run the test suite).
|
||||
- Add yourself to doc/contributors.rst `:)`
|
||||
|
||||
@ -153,11 +154,11 @@ To run a manual hook like `mypy` manually, run:
|
||||
pre-commit run --all-files --hook-stage manual mypy
|
||||
```
|
||||
|
||||
Typically we use `tox` to run the linters, e.g.
|
||||
Typically we use `hatch` to run the linters, e.g.
|
||||
|
||||
```bash
|
||||
tox -e typecheck-mypy
|
||||
tox -e lint-manual
|
||||
hatch run typing:check-mypy
|
||||
hatch run lint:build-manual
|
||||
```
|
||||
|
||||
## Documentation
|
||||
@ -178,13 +179,13 @@ documentation including narrative docs, and the [Sphinx docstring format](https:
|
||||
You can build the documentation locally by running:
|
||||
|
||||
```bash
|
||||
tox -e doc
|
||||
hatch run doc:build
|
||||
```
|
||||
|
||||
When updating docs, it can be helpful to run the live docs server as:
|
||||
|
||||
```bash
|
||||
tox -e doc-serve
|
||||
hatch run doc:serve
|
||||
```
|
||||
|
||||
Browse to the link provided, and then as you make changes to docstrings or narrative docs,
|
||||
@ -194,13 +195,13 @@ the pages will re-render and the browser will automatically refresh.
|
||||
## Running Tests Locally
|
||||
|
||||
- Ensure you have started the appropriate Mongo Server(s).
|
||||
- Run `pip install tox` to use `tox` for testing or run
|
||||
- Run `pip install hatch` to use `hatch` for testing or run
|
||||
`pip install -e ".[test]"` to run `pytest` directly.
|
||||
- Run `tox -m test` or `pytest` to run all of the tests.
|
||||
- Run `hatch run test:test` or `pytest` to run all of the tests.
|
||||
- Append `test/<mod_name>.py::<class_name>::<test_name>` to run
|
||||
specific tests. You can omit the `<test_name>` to test a full class
|
||||
and the `<class_name>` to test a full module. For example:
|
||||
`tox -m test -- test/test_change_stream.py::TestUnifiedChangeStreamsErrors::test_change_stream_errors_on_ElectionInProgress`.
|
||||
`hatch run test:test -- test/test_change_stream.py::TestUnifiedChangeStreamsErrors::test_change_stream_errors_on_ElectionInProgress`.
|
||||
- Use the `-k` argument to select tests by pattern.
|
||||
|
||||
## Running Load Balancer Tests Locally
|
||||
@ -213,15 +214,15 @@ the pages will re-render and the browser will automatically refresh.
|
||||
- Start the load balancer using:
|
||||
`MONGODB_URI='mongodb://localhost:27017,localhost:27018/' $PWD/drivers-evergreen-tools/.evergreen/run-load-balancer.sh start`.
|
||||
- Run the tests from the `pymongo` checkout directory using:
|
||||
`TEST_LOADBALANCER=1 tox -m test-eg`.
|
||||
`TEST_LOADBALANCER=1 hatch run test:test-eg`.
|
||||
|
||||
## Running Encryption Tests Locally
|
||||
- Clone `drivers-evergreen-tools`:
|
||||
`git clone git@github.com:mongodb-labs/drivers-evergreen-tools.git`.
|
||||
- Run `export DRIVERS_TOOLS=$PWD/drivers-evergreen-tools`
|
||||
- Run `AWS_PROFILE=<profile> tox -m setup-encryption` after setting up your AWS profile with `aws configure sso`.
|
||||
- Run the tests with `TEST_ENCRYPTION=1 tox -e test-eg`.
|
||||
- When done, run `tox -m teardown-encryption` to clean up.
|
||||
- Run `AWS_PROFILE=<profile> hatch run encryption:setup` after setting up your AWS profile with `aws configure sso`.
|
||||
- Run the tests with `TEST_ENCRYPTION=1 hatch run test:test-eg`.
|
||||
- When done, run `hatch run encryption:teardown` to clean up.
|
||||
|
||||
## Re-sync Spec Tests
|
||||
|
||||
|
||||
@ -155,7 +155,7 @@ python -m pip install "pymongo[gssapi,aws,ocsp,snappy,zstd,encryption]"
|
||||
Additional dependencies are:
|
||||
|
||||
- (to generate documentation or run tests)
|
||||
[tox](https://tox.wiki/en/latest/index.html)
|
||||
[hatch](https://hatch.pypa.io/dev/)
|
||||
|
||||
## Examples
|
||||
|
||||
@ -201,7 +201,7 @@ ObjectId('4aba160ee23f6b543e000002')
|
||||
Documentation is available at
|
||||
[pymongo.readthedocs.io](https://pymongo.readthedocs.io/en/stable/).
|
||||
|
||||
Documentation can be generated by running **tox -m doc**. Generated
|
||||
Documentation can be generated by running **pip install hatch; hatch run doc:build**. Generated
|
||||
documentation can be found in the `doc/build/html/` directory.
|
||||
|
||||
## Learning Resources
|
||||
@ -213,9 +213,10 @@ Center](https://www.mongodb.com/developer/languages/python/).
|
||||
|
||||
## Testing
|
||||
|
||||
The easiest way to run the tests is to run **tox -m test** in the root
|
||||
The easiest way to run the tests is to run *hatch run test:test** in the root
|
||||
of the distribution. For example,
|
||||
|
||||
```bash
|
||||
tox -e test
|
||||
pip install hatch
|
||||
hatch run test:test
|
||||
```
|
||||
|
||||
@ -105,8 +105,8 @@ following command from the root directory of the **PyMongo** source:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install tox
|
||||
$ tox -m doc
|
||||
$ pip install hatch
|
||||
$ hatch run doc:build
|
||||
|
||||
Indices and tables
|
||||
------------------
|
||||
|
||||
51
hatch.toml
Normal file
51
hatch.toml
Normal file
@ -0,0 +1,51 @@
|
||||
# See https://hatch.pypa.io/dev/config/environment/overview/
|
||||
|
||||
[envs.doc]
|
||||
features = ["docs"]
|
||||
[envs.doc.scripts]
|
||||
build = "sphinx-build -W -b html doc ./doc/_build/html"
|
||||
serve = "sphinx-autobuild -W -b html doc --watch ./pymongo --watch ./bson --watch ./gridfs ./doc/_build/serve"
|
||||
linkcheck = "sphinx-build -E -b linkcheck doc ./doc/_build/linkcheck"
|
||||
|
||||
[envs.doctest]
|
||||
features = ["docs","test"]
|
||||
[envs.doctest.scripts]
|
||||
test = "sphinx-build -E -b doctest doc ./doc/_build/doctest"
|
||||
|
||||
[envs.typing]
|
||||
features = ["encryption", "ocsp", "zstd", "aws"]
|
||||
dependencies = ["mypy==1.2.0","pyright==1.1.290", "certifi", "typing_extensions"]
|
||||
[envs.typing.scripts]
|
||||
check-mypy = [
|
||||
"mypy --install-types --non-interactive bson gridfs tools pymongo",
|
||||
"mypy --install-types --non-interactive --config-file mypy_test.ini test",
|
||||
"mypy --install-types --non-interactive test/test_typing.py test/test_typing_strict.py"
|
||||
]
|
||||
check-pyright = ["rm -f pyrightconfig.json", "pyright test/test_typing.py test/test_typing_strict.py"]
|
||||
check-strict-pyright = [
|
||||
"echo '{{\"strict\": [\"tests/test_typing_strict.py\"]}}' > pyrightconfig.json",
|
||||
"pyright test/test_typing_strict.py",
|
||||
"rm -f pyrightconfig.json"
|
||||
]
|
||||
check = ["check-mypy", "check-pyright", "check-strict-pyright"]
|
||||
|
||||
[envs.lint]
|
||||
skip-install = true
|
||||
dependencies = ["pre-commit"]
|
||||
[envs.lint.scripts]
|
||||
run = "pre-commit run --all-files"
|
||||
run-manual = "pre-commit run --all-files --hook-stage manual"
|
||||
|
||||
[envs.test]
|
||||
features = ["test"]
|
||||
[envs.test.scripts]
|
||||
test = "pytest -v --durations=5 --maxfail=10 {args}"
|
||||
test-eg = "bash ./.evergreen/run-tests.sh {args}"
|
||||
test-async = "test test/asynchronous/ {args}"
|
||||
test-mockupdb = ["pip install -U git+https://github.com/ajdavis/mongo-mockup-db@master", "test ./test/mockupdb"]
|
||||
|
||||
[envs.encryption]
|
||||
skip-install = true
|
||||
[envs.encryption.scripts]
|
||||
setup = "bash .evergreen/setup-encryption.sh"
|
||||
teardown = "bash .evergreen/teardown-encryption.sh"
|
||||
@ -299,7 +299,7 @@ class _EncryptionIO(AsyncMongoCryptCallback): # type: ignore[misc]
|
||||
self.client_ref = None
|
||||
self.key_vault_coll = None
|
||||
if self.mongocryptd_client:
|
||||
await self.mongocryptd_client.close()
|
||||
await self.mongocryptd_client.aclose()
|
||||
self.mongocryptd_client = None
|
||||
|
||||
|
||||
@ -439,7 +439,7 @@ class _Encrypter:
|
||||
self._closed = True
|
||||
await self._auto_encrypter.close()
|
||||
if self._internal_client:
|
||||
await self._internal_client.close()
|
||||
await self._internal_client.aclose()
|
||||
self._internal_client = None
|
||||
|
||||
|
||||
|
||||
@ -861,6 +861,10 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
# This will be used later if we fork.
|
||||
AsyncMongoClient._clients[self._topology._topology_id] = self
|
||||
|
||||
async def aconnect(self) -> None:
|
||||
"""Explicitly connect to MongoDB asynchronously instead of on the first operation."""
|
||||
await self._get_topology()
|
||||
|
||||
def _init_background(self, old_pid: Optional[int] = None) -> None:
|
||||
self._topology = Topology(self._topology_settings)
|
||||
# Seed the topology with the old one's pid so we can detect clients
|
||||
@ -1354,13 +1358,13 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
await self.close()
|
||||
await self.aclose()
|
||||
|
||||
# See PYTHON-3084.
|
||||
__iter__ = None
|
||||
|
||||
def __next__(self) -> NoReturn:
|
||||
raise TypeError("'MongoClient' object is not iterable")
|
||||
raise TypeError("'AsyncMongoClient' object is not iterable")
|
||||
|
||||
next = __next__
|
||||
|
||||
@ -1490,7 +1494,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
# command.
|
||||
pass
|
||||
|
||||
async def close(self) -> None:
|
||||
async def aclose(self) -> None:
|
||||
"""Cleanup client resources and disconnect from MongoDB.
|
||||
|
||||
End all server sessions created by this client by sending one or more
|
||||
|
||||
@ -860,6 +860,10 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
# This will be used later if we fork.
|
||||
MongoClient._clients[self._topology._topology_id] = self
|
||||
|
||||
def _connect(self) -> None:
|
||||
"""Explicitly connect to MongoDB synchronously instead of on the first operation."""
|
||||
self._get_topology()
|
||||
|
||||
def _init_background(self, old_pid: Optional[int] = None) -> None:
|
||||
self._topology = Topology(self._topology_settings)
|
||||
# Seed the topology with the old one's pid so we can detect clients
|
||||
|
||||
107
test/__init__.py
107
test/__init__.py
@ -17,6 +17,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import contextlib
|
||||
import gc
|
||||
import multiprocessing
|
||||
import os
|
||||
@ -39,8 +40,6 @@ from test.helpers import (
|
||||
TEST_SERVERLESS,
|
||||
TLS_OPTIONS,
|
||||
SystemCertsPatcher,
|
||||
_all_users,
|
||||
_create_user,
|
||||
client_knobs,
|
||||
db_pwd,
|
||||
db_user,
|
||||
@ -62,9 +61,9 @@ try:
|
||||
except ImportError:
|
||||
HAVE_IPADDRESS = False
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
from functools import partial, wraps
|
||||
from test.version import Version
|
||||
from typing import Any, Callable, Dict, Generator
|
||||
from typing import Any, Callable, Dict, Generator, overload
|
||||
from unittest import SkipTest
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
@ -812,6 +811,12 @@ class ClientContext:
|
||||
func=func,
|
||||
)
|
||||
|
||||
def require_sync(self, func):
|
||||
"""Run a test only if using the synchronous API."""
|
||||
return self._require(
|
||||
lambda: _IS_SYNC, "This test only works with the synchronous API", func=func
|
||||
)
|
||||
|
||||
def mongos_seeds(self):
|
||||
return ",".join("{}:{}".format(*address) for address in self.mongoses)
|
||||
|
||||
@ -919,6 +924,32 @@ class PyMongoTestCase(unittest.TestCase):
|
||||
self.assertEqual(proc.exitcode, 0)
|
||||
|
||||
|
||||
class UnitTest(PyMongoTestCase):
|
||||
"""Async base class for TestCases that don't require a connection to MongoDB."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if _IS_SYNC:
|
||||
cls._setup_class()
|
||||
else:
|
||||
asyncio.run(cls._setup_class())
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if _IS_SYNC:
|
||||
cls._tearDown_class()
|
||||
else:
|
||||
asyncio.run(cls._tearDown_class())
|
||||
|
||||
@classmethod
|
||||
def _setup_class(cls):
|
||||
cls._setup_class()
|
||||
|
||||
@classmethod
|
||||
def _tearDown_class(cls):
|
||||
cls._tearDown_class()
|
||||
|
||||
|
||||
class IntegrationTest(PyMongoTestCase):
|
||||
"""Async base class for TestCases that need a connection to MongoDB to pass."""
|
||||
|
||||
@ -933,6 +964,13 @@ class IntegrationTest(PyMongoTestCase):
|
||||
else:
|
||||
asyncio.run(cls._setup_class())
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if _IS_SYNC:
|
||||
cls._tearDown_class()
|
||||
else:
|
||||
asyncio.run(cls._tearDown_class())
|
||||
|
||||
@classmethod
|
||||
@client_context.require_connection
|
||||
def _setup_class(cls):
|
||||
@ -947,6 +985,10 @@ class IntegrationTest(PyMongoTestCase):
|
||||
else:
|
||||
cls.credentials = {}
|
||||
|
||||
@classmethod
|
||||
def _tearDown_class(cls):
|
||||
pass
|
||||
|
||||
def cleanup_colls(self, *collections):
|
||||
"""Cleanup collections faster than drop_collection."""
|
||||
for c in collections:
|
||||
@ -959,7 +1001,7 @@ class IntegrationTest(PyMongoTestCase):
|
||||
self.addCleanup(patcher.disable)
|
||||
|
||||
|
||||
class MockClientTest(unittest.TestCase):
|
||||
class MockClientTest(UnitTest):
|
||||
"""Base class for TestCases that use MockClient.
|
||||
|
||||
This class is *not* an IntegrationTest: if properly written, MockClient
|
||||
@ -972,8 +1014,26 @@ class MockClientTest(unittest.TestCase):
|
||||
# multiple seed addresses, or wait for heartbeat events are incompatible
|
||||
# with loadBalanced=True.
|
||||
@classmethod
|
||||
@client_context.require_no_load_balancer
|
||||
def setUpClass(cls):
|
||||
if _IS_SYNC:
|
||||
cls._setup_class()
|
||||
else:
|
||||
asyncio.run(cls._setup_class())
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if _IS_SYNC:
|
||||
cls._tearDown_class()
|
||||
else:
|
||||
asyncio.run(cls._tearDown_class())
|
||||
|
||||
@classmethod
|
||||
@client_context.require_no_load_balancer
|
||||
def _setup_class(cls):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def _tearDown_class(cls):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
@ -1051,3 +1111,38 @@ def print_running_clients():
|
||||
processed.add(obj._topology_id)
|
||||
except ReferenceError:
|
||||
pass
|
||||
|
||||
|
||||
def _all_users(db):
|
||||
return {u["user"] for u in (db.command("usersInfo")).get("users", [])}
|
||||
|
||||
|
||||
def _create_user(authdb, user, pwd=None, roles=None, **kwargs):
|
||||
cmd = SON([("createUser", user)])
|
||||
# X509 doesn't use a password
|
||||
if pwd:
|
||||
cmd["pwd"] = pwd
|
||||
cmd["roles"] = roles or ["root"]
|
||||
cmd.update(**kwargs)
|
||||
return authdb.command(cmd)
|
||||
|
||||
|
||||
def connected(client):
|
||||
"""Convenience to wait for a newly-constructed client to connect."""
|
||||
with warnings.catch_warnings():
|
||||
# Ignore warning that ping is always routed to primary even
|
||||
# if client's read preference isn't PRIMARY.
|
||||
warnings.simplefilter("ignore", UserWarning)
|
||||
client.admin.command("ping") # Force connection.
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def drop_collections(db: Database):
|
||||
# Drop all non-system collections in this database.
|
||||
for coll in db.list_collection_names(filter={"name": {"$regex": r"^(?!system\.)"}}):
|
||||
db.drop_collection(coll)
|
||||
|
||||
|
||||
def remove_all_users(db: Database):
|
||||
db.command("dropAllUsersFromDatabase", 1, writeConcern={"w": client_context.w})
|
||||
|
||||
@ -17,6 +17,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import contextlib
|
||||
import gc
|
||||
import multiprocessing
|
||||
import os
|
||||
@ -39,8 +40,6 @@ from test.helpers import (
|
||||
TEST_SERVERLESS,
|
||||
TLS_OPTIONS,
|
||||
SystemCertsPatcher,
|
||||
_all_users,
|
||||
_create_user,
|
||||
client_knobs,
|
||||
db_pwd,
|
||||
db_user,
|
||||
@ -62,9 +61,9 @@ try:
|
||||
except ImportError:
|
||||
HAVE_IPADDRESS = False
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from functools import wraps
|
||||
from functools import partial, wraps
|
||||
from test.version import Version
|
||||
from typing import Any, Callable, Dict, Generator
|
||||
from typing import Any, Callable, Dict, Generator, overload
|
||||
from unittest import SkipTest
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
@ -184,7 +183,7 @@ class AsyncClientContext:
|
||||
self.connection_attempts.append(f"failed to connect client {client!r}: {exc}")
|
||||
return None
|
||||
finally:
|
||||
await client.close()
|
||||
await client.aclose()
|
||||
|
||||
async def _init_client(self):
|
||||
self.client = await self._connect(host, port)
|
||||
@ -229,7 +228,7 @@ class AsyncClientContext:
|
||||
if not self.serverless and not IS_SRV:
|
||||
# See if db_user already exists.
|
||||
if not await self._check_user_provided():
|
||||
_create_user(self.client.admin, db_user, db_pwd)
|
||||
await _create_user(self.client.admin, db_user, db_pwd)
|
||||
|
||||
self.client = await self._connect(
|
||||
host,
|
||||
@ -304,7 +303,7 @@ class AsyncClientContext:
|
||||
params = self.cmd_line["parsed"].get("setParameter", {})
|
||||
if params.get("enableTestCommands") == "1":
|
||||
self.test_commands_enabled = True
|
||||
self.has_ipv6 = self._server_started_with_ipv6()
|
||||
self.has_ipv6 = await self._server_started_with_ipv6()
|
||||
|
||||
self.is_mongos = (await self.hello).get("msg") == "isdbgrid"
|
||||
if self.is_mongos:
|
||||
@ -390,7 +389,7 @@ class AsyncClientContext:
|
||||
)
|
||||
|
||||
try:
|
||||
return db_user in _all_users(client.admin)
|
||||
return db_user in await _all_users(client.admin)
|
||||
except pymongo.errors.OperationFailure as e:
|
||||
assert e.details is not None
|
||||
msg = e.details.get("errmsg", "")
|
||||
@ -400,7 +399,7 @@ class AsyncClientContext:
|
||||
else:
|
||||
raise
|
||||
finally:
|
||||
await client.close()
|
||||
await client.aclose()
|
||||
|
||||
def _server_started_with_auth(self):
|
||||
# MongoDB >= 2.0
|
||||
@ -482,9 +481,9 @@ class AsyncClientContext:
|
||||
return decorate
|
||||
return make_wrapper(func)
|
||||
|
||||
def create_user(self, dbname, user, pwd=None, roles=None, **kwargs):
|
||||
async def create_user(self, dbname, user, pwd=None, roles=None, **kwargs):
|
||||
kwargs["writeConcern"] = {"w": self.w}
|
||||
return _create_user(self.client[dbname], user, pwd, roles, **kwargs)
|
||||
return await _create_user(self.client[dbname], user, pwd, roles, **kwargs)
|
||||
|
||||
async def drop_user(self, dbname, user):
|
||||
await self.client[dbname].command("dropUser", user, writeConcern={"w": self.w})
|
||||
@ -814,6 +813,12 @@ class AsyncClientContext:
|
||||
func=func,
|
||||
)
|
||||
|
||||
def require_sync(self, func):
|
||||
"""Run a test only if using the synchronous API."""
|
||||
return self._require(
|
||||
lambda: _IS_SYNC, "This test only works with the synchronous API", func=func
|
||||
)
|
||||
|
||||
def mongos_seeds(self):
|
||||
return ",".join("{}:{}".format(*address) for address in self.mongoses)
|
||||
|
||||
@ -921,6 +926,32 @@ class AsyncPyMongoTestCase(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(proc.exitcode, 0)
|
||||
|
||||
|
||||
class AsyncUnitTest(AsyncPyMongoTestCase):
|
||||
"""Async base class for TestCases that don't require a connection to MongoDB."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if _IS_SYNC:
|
||||
cls._setup_class()
|
||||
else:
|
||||
asyncio.run(cls._setup_class())
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if _IS_SYNC:
|
||||
cls._tearDown_class()
|
||||
else:
|
||||
asyncio.run(cls._tearDown_class())
|
||||
|
||||
@classmethod
|
||||
async def _setup_class(cls):
|
||||
await cls._setup_class()
|
||||
|
||||
@classmethod
|
||||
async def _tearDown_class(cls):
|
||||
await cls._tearDown_class()
|
||||
|
||||
|
||||
class AsyncIntegrationTest(AsyncPyMongoTestCase):
|
||||
"""Async base class for TestCases that need a connection to MongoDB to pass."""
|
||||
|
||||
@ -935,6 +966,13 @@ class AsyncIntegrationTest(AsyncPyMongoTestCase):
|
||||
else:
|
||||
asyncio.run(cls._setup_class())
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if _IS_SYNC:
|
||||
cls._tearDown_class()
|
||||
else:
|
||||
asyncio.run(cls._tearDown_class())
|
||||
|
||||
@classmethod
|
||||
@async_client_context.require_connection
|
||||
async def _setup_class(cls):
|
||||
@ -949,6 +987,10 @@ class AsyncIntegrationTest(AsyncPyMongoTestCase):
|
||||
else:
|
||||
cls.credentials = {}
|
||||
|
||||
@classmethod
|
||||
async def _tearDown_class(cls):
|
||||
pass
|
||||
|
||||
async def cleanup_colls(self, *collections):
|
||||
"""Cleanup collections faster than drop_collection."""
|
||||
for c in collections:
|
||||
@ -961,7 +1003,7 @@ class AsyncIntegrationTest(AsyncPyMongoTestCase):
|
||||
self.addCleanup(patcher.disable)
|
||||
|
||||
|
||||
class AsyncMockClientTest(unittest.TestCase):
|
||||
class AsyncMockClientTest(AsyncUnitTest):
|
||||
"""Base class for TestCases that use MockClient.
|
||||
|
||||
This class is *not* an IntegrationTest: if properly written, MockClient
|
||||
@ -974,8 +1016,26 @@ class AsyncMockClientTest(unittest.TestCase):
|
||||
# multiple seed addresses, or wait for heartbeat events are incompatible
|
||||
# with loadBalanced=True.
|
||||
@classmethod
|
||||
@async_client_context.require_no_load_balancer
|
||||
def setUpClass(cls):
|
||||
if _IS_SYNC:
|
||||
cls._setup_class()
|
||||
else:
|
||||
asyncio.run(cls._setup_class())
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if _IS_SYNC:
|
||||
cls._tearDown_class()
|
||||
else:
|
||||
asyncio.run(cls._tearDown_class())
|
||||
|
||||
@classmethod
|
||||
@async_client_context.require_no_load_balancer
|
||||
async def _setup_class(cls):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
async def _tearDown_class(cls):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
@ -1015,7 +1075,7 @@ async def async_teardown():
|
||||
await c.drop_database("pymongo_test2")
|
||||
await c.drop_database("pymongo_test_mike")
|
||||
await c.drop_database("pymongo_test_bernie")
|
||||
await c.close()
|
||||
await c.aclose()
|
||||
|
||||
print_running_clients()
|
||||
|
||||
@ -1053,3 +1113,38 @@ def print_running_clients():
|
||||
processed.add(obj._topology_id)
|
||||
except ReferenceError:
|
||||
pass
|
||||
|
||||
|
||||
async def _all_users(db):
|
||||
return {u["user"] for u in (await db.command("usersInfo")).get("users", [])}
|
||||
|
||||
|
||||
async def _create_user(authdb, user, pwd=None, roles=None, **kwargs):
|
||||
cmd = SON([("createUser", user)])
|
||||
# X509 doesn't use a password
|
||||
if pwd:
|
||||
cmd["pwd"] = pwd
|
||||
cmd["roles"] = roles or ["root"]
|
||||
cmd.update(**kwargs)
|
||||
return await authdb.command(cmd)
|
||||
|
||||
|
||||
async def connected(client):
|
||||
"""Convenience to wait for a newly-constructed client to connect."""
|
||||
with warnings.catch_warnings():
|
||||
# Ignore warning that ping is always routed to primary even
|
||||
# if client's read preference isn't PRIMARY.
|
||||
warnings.simplefilter("ignore", UserWarning)
|
||||
await client.admin.command("ping") # Force connection.
|
||||
|
||||
return client
|
||||
|
||||
|
||||
async def drop_collections(db: AsyncDatabase):
|
||||
# Drop all non-system collections in this database.
|
||||
for coll in await db.list_collection_names(filter={"name": {"$regex": r"^(?!system\.)"}}):
|
||||
await db.drop_collection(coll)
|
||||
|
||||
|
||||
async def remove_all_users(db: AsyncDatabase):
|
||||
await db.command("dropAllUsersFromDatabase", 1, writeConcern={"w": async_client_context.w})
|
||||
|
||||
252
test/asynchronous/pymongo_mocks.py
Normal file
252
test/asynchronous/pymongo_mocks.py
Normal file
@ -0,0 +1,252 @@
|
||||
# Copyright 2013-present MongoDB, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tools for mocking parts of PyMongo to test other parts."""
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import weakref
|
||||
from functools import partial
|
||||
from test import client_context
|
||||
from test.asynchronous import async_client_context
|
||||
|
||||
from pymongo import AsyncMongoClient, common
|
||||
from pymongo.asynchronous.monitor import Monitor
|
||||
from pymongo.asynchronous.pool import Pool
|
||||
from pymongo.errors import AutoReconnect, NetworkTimeout
|
||||
from pymongo.hello import Hello, HelloCompat
|
||||
from pymongo.server_description import ServerDescription
|
||||
|
||||
_IS_SYNC = False
|
||||
|
||||
|
||||
class MockPool(Pool):
|
||||
def __init__(self, client, pair, *args, **kwargs):
|
||||
# MockPool gets a 'client' arg, regular pools don't. Weakref it to
|
||||
# avoid cycle with __del__, causing ResourceWarnings in Python 3.3.
|
||||
self.client = weakref.proxy(client)
|
||||
self.mock_host, self.mock_port = pair
|
||||
|
||||
# Actually connect to the default server.
|
||||
Pool.__init__(self, (client_context.host, client_context.port), *args, **kwargs)
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def checkout(self, handler=None):
|
||||
client = self.client
|
||||
host_and_port = f"{self.mock_host}:{self.mock_port}"
|
||||
if host_and_port in client.mock_down_hosts:
|
||||
raise AutoReconnect("mock error")
|
||||
|
||||
assert host_and_port in (
|
||||
client.mock_standalones + client.mock_members + client.mock_mongoses
|
||||
), "bad host: %s" % host_and_port
|
||||
|
||||
async with Pool.checkout(self, handler) as conn:
|
||||
conn.mock_host = self.mock_host
|
||||
conn.mock_port = self.mock_port
|
||||
yield conn
|
||||
|
||||
|
||||
class DummyMonitor:
|
||||
def __init__(self, server_description, topology, pool, topology_settings):
|
||||
self._server_description = server_description
|
||||
self.opened = False
|
||||
|
||||
def cancel_check(self):
|
||||
pass
|
||||
|
||||
def join(self):
|
||||
pass
|
||||
|
||||
def open(self):
|
||||
self.opened = True
|
||||
|
||||
def request_check(self):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
self.opened = False
|
||||
|
||||
|
||||
class AsyncMockMonitor(Monitor):
|
||||
def __init__(self, client, server_description, topology, pool, topology_settings):
|
||||
# MockMonitor gets a 'client' arg, regular monitors don't. Weakref it
|
||||
# to avoid cycles.
|
||||
self.client = weakref.proxy(client)
|
||||
Monitor.__init__(self, server_description, topology, pool, topology_settings)
|
||||
|
||||
async def _check_once(self):
|
||||
client = self.client
|
||||
address = self._server_description.address
|
||||
response, rtt = client.mock_hello("%s:%d" % address) # type: ignore[str-format]
|
||||
return ServerDescription(address, Hello(response), rtt)
|
||||
|
||||
|
||||
class AsyncMockClient(AsyncMongoClient):
|
||||
def __init__(
|
||||
self,
|
||||
standalones,
|
||||
members,
|
||||
mongoses,
|
||||
hello_hosts=None,
|
||||
arbiters=None,
|
||||
down_hosts=None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
"""An AsyncMongoClient connected to the default server, with a mock topology.
|
||||
|
||||
standalones, members, mongoses, arbiters, and down_hosts determine the
|
||||
configuration of the topology. They are formatted like ['a:1', 'b:2'].
|
||||
hello_hosts provides an alternative host list for the server's
|
||||
mocked hello response; see test_connect_with_internal_ips.
|
||||
"""
|
||||
self.mock_standalones = standalones[:]
|
||||
self.mock_members = members[:]
|
||||
|
||||
if self.mock_members:
|
||||
self.mock_primary = self.mock_members[0]
|
||||
else:
|
||||
self.mock_primary = None
|
||||
|
||||
# Hosts that should be considered an arbiter.
|
||||
self.mock_arbiters = arbiters[:] if arbiters else []
|
||||
|
||||
if hello_hosts is not None:
|
||||
self.mock_hello_hosts = hello_hosts
|
||||
else:
|
||||
self.mock_hello_hosts = members[:]
|
||||
|
||||
self.mock_mongoses = mongoses[:]
|
||||
|
||||
# Hosts that should raise socket errors.
|
||||
self.mock_down_hosts = down_hosts[:] if down_hosts else []
|
||||
|
||||
# Hostname -> (min wire version, max wire version)
|
||||
self.mock_wire_versions = {}
|
||||
|
||||
# Hostname -> max write batch size
|
||||
self.mock_max_write_batch_sizes = {}
|
||||
|
||||
# Hostname -> round trip time
|
||||
self.mock_rtts = {}
|
||||
|
||||
kwargs["_pool_class"] = partial(MockPool, self)
|
||||
kwargs["_monitor_class"] = partial(AsyncMockMonitor, self)
|
||||
|
||||
client_options = async_client_context.default_client_options.copy()
|
||||
client_options.update(kwargs)
|
||||
|
||||
super().__init__(*args, **client_options)
|
||||
|
||||
@classmethod
|
||||
async def get_async_mock_client(
|
||||
cls,
|
||||
standalones,
|
||||
members,
|
||||
mongoses,
|
||||
hello_hosts=None,
|
||||
arbiters=None,
|
||||
down_hosts=None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
c = AsyncMockClient(
|
||||
standalones, members, mongoses, hello_hosts, arbiters, down_hosts, *args, **kwargs
|
||||
)
|
||||
|
||||
await c.aconnect()
|
||||
return c
|
||||
|
||||
def kill_host(self, host):
|
||||
"""Host is like 'a:1'."""
|
||||
self.mock_down_hosts.append(host)
|
||||
|
||||
def revive_host(self, host):
|
||||
"""Host is like 'a:1'."""
|
||||
self.mock_down_hosts.remove(host)
|
||||
|
||||
def set_wire_version_range(self, host, min_version, max_version):
|
||||
self.mock_wire_versions[host] = (min_version, max_version)
|
||||
|
||||
def set_max_write_batch_size(self, host, size):
|
||||
self.mock_max_write_batch_sizes[host] = size
|
||||
|
||||
def mock_hello(self, host):
|
||||
"""Return mock hello response (a dict) and round trip time."""
|
||||
if host in self.mock_wire_versions:
|
||||
min_wire_version, max_wire_version = self.mock_wire_versions[host]
|
||||
else:
|
||||
min_wire_version = common.MIN_SUPPORTED_WIRE_VERSION
|
||||
max_wire_version = common.MAX_SUPPORTED_WIRE_VERSION
|
||||
|
||||
max_write_batch_size = self.mock_max_write_batch_sizes.get(
|
||||
host, common.MAX_WRITE_BATCH_SIZE
|
||||
)
|
||||
|
||||
rtt = self.mock_rtts.get(host, 0)
|
||||
|
||||
# host is like 'a:1'.
|
||||
if host in self.mock_down_hosts:
|
||||
raise NetworkTimeout("mock timeout")
|
||||
|
||||
elif host in self.mock_standalones:
|
||||
response = {
|
||||
"ok": 1,
|
||||
HelloCompat.LEGACY_CMD: True,
|
||||
"minWireVersion": min_wire_version,
|
||||
"maxWireVersion": max_wire_version,
|
||||
"maxWriteBatchSize": max_write_batch_size,
|
||||
}
|
||||
elif host in self.mock_members:
|
||||
primary = host == self.mock_primary
|
||||
|
||||
# Simulate a replica set member.
|
||||
response = {
|
||||
"ok": 1,
|
||||
HelloCompat.LEGACY_CMD: primary,
|
||||
"secondary": not primary,
|
||||
"setName": "rs",
|
||||
"hosts": self.mock_hello_hosts,
|
||||
"minWireVersion": min_wire_version,
|
||||
"maxWireVersion": max_wire_version,
|
||||
"maxWriteBatchSize": max_write_batch_size,
|
||||
}
|
||||
|
||||
if self.mock_primary:
|
||||
response["primary"] = self.mock_primary
|
||||
|
||||
if host in self.mock_arbiters:
|
||||
response["arbiterOnly"] = True
|
||||
response["secondary"] = False
|
||||
elif host in self.mock_mongoses:
|
||||
response = {
|
||||
"ok": 1,
|
||||
HelloCompat.LEGACY_CMD: True,
|
||||
"minWireVersion": min_wire_version,
|
||||
"maxWireVersion": max_wire_version,
|
||||
"msg": "isdbgrid",
|
||||
"maxWriteBatchSize": max_write_batch_size,
|
||||
}
|
||||
else:
|
||||
# In test_internal_ips(), we try to connect to a host listed
|
||||
# in hello['hosts'] but not publicly accessible.
|
||||
raise AutoReconnect("Unknown host: %s" % host)
|
||||
|
||||
return response, rtt
|
||||
|
||||
def _process_periodic_tasks(self):
|
||||
# Avoid the background thread causing races, e.g. a surprising
|
||||
# reconnect while we're trying to test a disconnected client.
|
||||
pass
|
||||
2500
test/asynchronous/test_client.py
Normal file
2500
test/asynchronous/test_client.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -1825,7 +1825,7 @@ class AsyncTestCollection(AsyncIntegrationTest):
|
||||
await self.db.test.insert_many([{"i": i} for i in range(150)])
|
||||
|
||||
client = await async_rs_or_single_client(maxPoolSize=1)
|
||||
self.addAsyncCleanup(client.close)
|
||||
self.addAsyncCleanup(client.aclose)
|
||||
pool = await async_get_pool(client)
|
||||
|
||||
# Make sure the socket is returned after exhaustion.
|
||||
|
||||
@ -236,7 +236,7 @@ class TestDatabase(AsyncIntegrationTest):
|
||||
async def test_check_exists(self):
|
||||
listener = OvertCommandListener()
|
||||
client = await async_rs_or_single_client(event_listeners=[listener])
|
||||
self.addAsyncCleanup(client.close)
|
||||
self.addAsyncCleanup(client.aclose)
|
||||
db = client[self.db.name]
|
||||
await db.drop_collection("unique")
|
||||
await db.create_collection("unique", check_exists=True)
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Shared constants and helper method for pymongo, bson, and gridfs test suites."""
|
||||
"""Shared constants and helper methods for pymongo, bson, and gridfs test suites."""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
|
||||
@ -20,14 +20,15 @@ import weakref
|
||||
from functools import partial
|
||||
from test import client_context
|
||||
|
||||
from pymongo import common
|
||||
from pymongo import MongoClient, common
|
||||
from pymongo.errors import AutoReconnect, NetworkTimeout
|
||||
from pymongo.hello import Hello, HelloCompat
|
||||
from pymongo.server_description import ServerDescription
|
||||
from pymongo.synchronous.mongo_client import MongoClient
|
||||
from pymongo.synchronous.monitor import Monitor
|
||||
from pymongo.synchronous.pool import Pool
|
||||
|
||||
_IS_SYNC = True
|
||||
|
||||
|
||||
class MockPool(Pool):
|
||||
def __init__(self, client, pair, *args, **kwargs):
|
||||
@ -77,7 +78,7 @@ class DummyMonitor:
|
||||
self.opened = False
|
||||
|
||||
|
||||
class MockMonitor(Monitor):
|
||||
class SyncMockMonitor(Monitor):
|
||||
def __init__(self, client, server_description, topology, pool, topology_settings):
|
||||
# MockMonitor gets a 'client' arg, regular monitors don't. Weakref it
|
||||
# to avoid cycles.
|
||||
@ -141,13 +142,32 @@ class MockClient(MongoClient):
|
||||
self.mock_rtts = {}
|
||||
|
||||
kwargs["_pool_class"] = partial(MockPool, self)
|
||||
kwargs["_monitor_class"] = partial(MockMonitor, self)
|
||||
kwargs["_monitor_class"] = partial(SyncMockMonitor, self)
|
||||
|
||||
client_options = client_context.default_client_options.copy()
|
||||
client_options.update(kwargs)
|
||||
|
||||
super().__init__(*args, **client_options)
|
||||
|
||||
@classmethod
|
||||
def get_mock_client(
|
||||
cls,
|
||||
standalones,
|
||||
members,
|
||||
mongoses,
|
||||
hello_hosts=None,
|
||||
arbiters=None,
|
||||
down_hosts=None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
c = MockClient(
|
||||
standalones, members, mongoses, hello_hosts, arbiters, down_hosts, *args, **kwargs
|
||||
)
|
||||
|
||||
c._connect()
|
||||
return c
|
||||
|
||||
def kill_host(self, host):
|
||||
"""Host is like 'a:1'."""
|
||||
self.mock_down_hosts.append(host)
|
||||
|
||||
@ -23,9 +23,8 @@ from pymongo.synchronous.mongo_client import MongoClient
|
||||
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
from test import IntegrationTest, client_context, unittest
|
||||
from test import IntegrationTest, client_context, remove_all_users, unittest
|
||||
from test.utils import (
|
||||
remove_all_users,
|
||||
rs_or_single_client_noauth,
|
||||
single_client,
|
||||
wait_until,
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import _thread as thread
|
||||
import asyncio
|
||||
import contextlib
|
||||
import copy
|
||||
import datetime
|
||||
@ -45,10 +46,13 @@ from test import (
|
||||
IntegrationTest,
|
||||
MockClientTest,
|
||||
SkipTest,
|
||||
UnitTest,
|
||||
client_context,
|
||||
client_knobs,
|
||||
connected,
|
||||
db_pwd,
|
||||
db_user,
|
||||
remove_all_users,
|
||||
unittest,
|
||||
)
|
||||
from test.pymongo_mocks import MockClient
|
||||
@ -57,14 +61,12 @@ from test.utils import (
|
||||
CMAPListener,
|
||||
FunctionCallRecorder,
|
||||
assertRaisesExactly,
|
||||
connected,
|
||||
delay,
|
||||
get_pool,
|
||||
gevent_monkey_patched,
|
||||
is_greenthread_patched,
|
||||
lazy_client_trial,
|
||||
one,
|
||||
remove_all_users,
|
||||
rs_client,
|
||||
rs_or_single_client,
|
||||
rs_or_single_client_noauth,
|
||||
@ -109,6 +111,7 @@ from pymongo.server_type import SERVER_TYPE
|
||||
from pymongo.synchronous.command_cursor import CommandCursor
|
||||
from pymongo.synchronous.cursor import Cursor, CursorType
|
||||
from pymongo.synchronous.database import Database
|
||||
from pymongo.synchronous.helpers import next
|
||||
from pymongo.synchronous.mongo_client import MongoClient
|
||||
from pymongo.synchronous.pool import (
|
||||
Connection,
|
||||
@ -118,18 +121,20 @@ from pymongo.synchronous.topology import _ErrorContext
|
||||
from pymongo.topology_description import TopologyDescription
|
||||
from pymongo.write_concern import WriteConcern
|
||||
|
||||
_IS_SYNC = True
|
||||
|
||||
class ClientUnitTest(unittest.TestCase):
|
||||
|
||||
class ClientUnitTest(UnitTest):
|
||||
"""MongoClient tests that don't require a server."""
|
||||
|
||||
client: MongoClient
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
def _setup_class(cls):
|
||||
cls.client = rs_or_single_client(connect=False, serverSelectionTimeoutMS=100)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
def _tearDown_class(cls):
|
||||
cls.client.close()
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@ -254,7 +259,8 @@ class ClientUnitTest(unittest.TestCase):
|
||||
|
||||
def test_get_default_database(self):
|
||||
c = rs_or_single_client(
|
||||
"mongodb://%s:%d/foo" % (client_context.host, client_context.port), connect=False
|
||||
"mongodb://%s:%d/foo" % (client_context.host, client_context.port),
|
||||
connect=False,
|
||||
)
|
||||
self.assertEqual(Database(c, "foo"), c.get_default_database())
|
||||
# Test that default doesn't override the URI value.
|
||||
@ -269,39 +275,49 @@ class ClientUnitTest(unittest.TestCase):
|
||||
self.assertEqual(write_concern, db.write_concern)
|
||||
|
||||
c = rs_or_single_client(
|
||||
"mongodb://%s:%d/" % (client_context.host, client_context.port), connect=False
|
||||
"mongodb://%s:%d/" % (client_context.host, client_context.port),
|
||||
connect=False,
|
||||
)
|
||||
self.assertEqual(Database(c, "foo"), c.get_default_database("foo"))
|
||||
|
||||
def test_get_default_database_error(self):
|
||||
# URI with no database.
|
||||
c = rs_or_single_client(
|
||||
"mongodb://%s:%d/" % (client_context.host, client_context.port), connect=False
|
||||
"mongodb://%s:%d/" % (client_context.host, client_context.port),
|
||||
connect=False,
|
||||
)
|
||||
self.assertRaises(ConfigurationError, c.get_default_database)
|
||||
|
||||
def test_get_default_database_with_authsource(self):
|
||||
# Ensure we distinguish database name from authSource.
|
||||
uri = "mongodb://%s:%d/foo?authSource=src" % (client_context.host, client_context.port)
|
||||
uri = "mongodb://%s:%d/foo?authSource=src" % (
|
||||
client_context.host,
|
||||
client_context.port,
|
||||
)
|
||||
c = rs_or_single_client(uri, connect=False)
|
||||
self.assertEqual(Database(c, "foo"), c.get_default_database())
|
||||
|
||||
def test_get_database_default(self):
|
||||
c = rs_or_single_client(
|
||||
"mongodb://%s:%d/foo" % (client_context.host, client_context.port), connect=False
|
||||
"mongodb://%s:%d/foo" % (client_context.host, client_context.port),
|
||||
connect=False,
|
||||
)
|
||||
self.assertEqual(Database(c, "foo"), c.get_database())
|
||||
|
||||
def test_get_database_default_error(self):
|
||||
# URI with no database.
|
||||
c = rs_or_single_client(
|
||||
"mongodb://%s:%d/" % (client_context.host, client_context.port), connect=False
|
||||
"mongodb://%s:%d/" % (client_context.host, client_context.port),
|
||||
connect=False,
|
||||
)
|
||||
self.assertRaises(ConfigurationError, c.get_database)
|
||||
|
||||
def test_get_database_default_with_authsource(self):
|
||||
# Ensure we distinguish database name from authSource.
|
||||
uri = "mongodb://%s:%d/foo?authSource=src" % (client_context.host, client_context.port)
|
||||
uri = "mongodb://%s:%d/foo?authSource=src" % (
|
||||
client_context.host,
|
||||
client_context.port,
|
||||
)
|
||||
c = rs_or_single_client(uri, connect=False)
|
||||
self.assertEqual(Database(c, "foo"), c.get_database())
|
||||
|
||||
@ -634,7 +650,7 @@ class TestClient(IntegrationTest):
|
||||
with client_knobs(kill_cursor_frequency=0.1):
|
||||
# Assert reaper doesn't remove connections when maxIdleTimeMS not set
|
||||
client = rs_or_single_client()
|
||||
server = client._get_topology().select_server(readable_server_selector, _Op.TEST)
|
||||
server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST)
|
||||
with server._pool.checkout() as conn:
|
||||
pass
|
||||
self.assertEqual(1, len(server._pool.conns))
|
||||
@ -645,7 +661,7 @@ class TestClient(IntegrationTest):
|
||||
with client_knobs(kill_cursor_frequency=0.1):
|
||||
# Assert reaper removes idle socket and replaces it with a new one
|
||||
client = rs_or_single_client(maxIdleTimeMS=500, minPoolSize=1)
|
||||
server = client._get_topology().select_server(readable_server_selector, _Op.TEST)
|
||||
server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST)
|
||||
with server._pool.checkout() as conn:
|
||||
pass
|
||||
# When the reaper runs at the same time as the get_socket, two
|
||||
@ -659,7 +675,7 @@ class TestClient(IntegrationTest):
|
||||
with client_knobs(kill_cursor_frequency=0.1):
|
||||
# Assert reaper respects maxPoolSize when adding new connections.
|
||||
client = rs_or_single_client(maxIdleTimeMS=500, minPoolSize=1, maxPoolSize=1)
|
||||
server = client._get_topology().select_server(readable_server_selector, _Op.TEST)
|
||||
server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST)
|
||||
with server._pool.checkout() as conn:
|
||||
pass
|
||||
# When the reaper runs at the same time as the get_socket,
|
||||
@ -673,7 +689,7 @@ class TestClient(IntegrationTest):
|
||||
with client_knobs(kill_cursor_frequency=0.1):
|
||||
# Assert reaper has removed idle socket and NOT replaced it
|
||||
client = rs_or_single_client(maxIdleTimeMS=500)
|
||||
server = client._get_topology().select_server(readable_server_selector, _Op.TEST)
|
||||
server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST)
|
||||
with server._pool.checkout() as conn_one:
|
||||
pass
|
||||
# Assert that the pool does not close connections prematurely.
|
||||
@ -690,12 +706,12 @@ class TestClient(IntegrationTest):
|
||||
def test_min_pool_size(self):
|
||||
with client_knobs(kill_cursor_frequency=0.1):
|
||||
client = rs_or_single_client()
|
||||
server = client._get_topology().select_server(readable_server_selector, _Op.TEST)
|
||||
server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST)
|
||||
self.assertEqual(0, len(server._pool.conns))
|
||||
|
||||
# Assert that pool started up at minPoolSize
|
||||
client = rs_or_single_client(minPoolSize=10)
|
||||
server = client._get_topology().select_server(readable_server_selector, _Op.TEST)
|
||||
server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST)
|
||||
wait_until(
|
||||
lambda: len(server._pool.conns) == 10,
|
||||
"pool initialized with 10 connections",
|
||||
@ -714,7 +730,7 @@ class TestClient(IntegrationTest):
|
||||
# Use high frequency to test _get_socket_no_auth.
|
||||
with client_knobs(kill_cursor_frequency=99999999):
|
||||
client = rs_or_single_client(maxIdleTimeMS=500)
|
||||
server = client._get_topology().select_server(readable_server_selector, _Op.TEST)
|
||||
server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST)
|
||||
with server._pool.checkout() as conn:
|
||||
pass
|
||||
self.assertEqual(1, len(server._pool.conns))
|
||||
@ -728,7 +744,7 @@ class TestClient(IntegrationTest):
|
||||
|
||||
# Test that connections are reused if maxIdleTimeMS is not set.
|
||||
client = rs_or_single_client()
|
||||
server = client._get_topology().select_server(readable_server_selector, _Op.TEST)
|
||||
server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST)
|
||||
with server._pool.checkout() as conn:
|
||||
pass
|
||||
self.assertEqual(1, len(server._pool.conns))
|
||||
@ -793,12 +809,14 @@ class TestClient(IntegrationTest):
|
||||
|
||||
bad_host = "somedomainthatdoesntexist.org"
|
||||
c = MongoClient(bad_host, port, connectTimeoutMS=1, serverSelectionTimeoutMS=10)
|
||||
self.assertRaises(ConnectionFailure, c.pymongo_test.test.find_one)
|
||||
with self.assertRaises(ConnectionFailure):
|
||||
c.pymongo_test.test.find_one()
|
||||
|
||||
def test_init_disconnected_with_auth(self):
|
||||
uri = "mongodb://user:pass@somedomainthatdoesntexist"
|
||||
c = MongoClient(uri, connectTimeoutMS=1, serverSelectionTimeoutMS=10)
|
||||
self.assertRaises(ConnectionFailure, c.pymongo_test.test.find_one)
|
||||
with self.assertRaises(ConnectionFailure):
|
||||
c.pymongo_test.test.find_one()
|
||||
|
||||
def test_equality(self):
|
||||
seed = "{}:{}".format(*list(self.client._topology_settings.seeds)[0])
|
||||
@ -816,7 +834,8 @@ class TestClient(IntegrationTest):
|
||||
self.assertNotEqual(MongoClient("a", connect=False), MongoClient("b", connect=False))
|
||||
# Same seeds but out of order still compares equal:
|
||||
self.assertEqual(
|
||||
MongoClient(["a", "b", "c"], connect=False), MongoClient(["c", "a", "b"], connect=False)
|
||||
MongoClient(["a", "b", "c"], connect=False),
|
||||
MongoClient(["c", "a", "b"], connect=False),
|
||||
)
|
||||
|
||||
def test_hashable(self):
|
||||
@ -830,9 +849,10 @@ class TestClient(IntegrationTest):
|
||||
|
||||
def test_host_w_port(self):
|
||||
with self.assertRaises(ValueError):
|
||||
host = client_context.host
|
||||
connected(
|
||||
MongoClient(
|
||||
f"{client_context.host}:1234567",
|
||||
f"{host}:1234567",
|
||||
connectTimeoutMS=1,
|
||||
serverSelectionTimeoutMS=10,
|
||||
)
|
||||
@ -883,10 +903,10 @@ class TestClient(IntegrationTest):
|
||||
wait_until(lambda: client_context.nodes == self.client.nodes, "find all nodes")
|
||||
|
||||
def test_list_databases(self):
|
||||
cmd_docs = self.client.admin.command("listDatabases")["databases"]
|
||||
cmd_docs = (self.client.admin.command("listDatabases"))["databases"]
|
||||
cursor = self.client.list_databases()
|
||||
self.assertIsInstance(cursor, CommandCursor)
|
||||
helper_docs = list(cursor)
|
||||
helper_docs = cursor.to_list()
|
||||
self.assertTrue(len(helper_docs) > 0)
|
||||
self.assertEqual(len(helper_docs), len(cmd_docs))
|
||||
# PYTHON-3529 Some fields may change between calls, just compare names.
|
||||
@ -900,7 +920,7 @@ class TestClient(IntegrationTest):
|
||||
|
||||
self.client.pymongo_test.test.insert_one({})
|
||||
cursor = self.client.list_databases(filter={"name": "admin"})
|
||||
docs = list(cursor)
|
||||
docs = cursor.to_list()
|
||||
self.assertEqual(1, len(docs))
|
||||
self.assertEqual(docs[0]["name"], "admin")
|
||||
|
||||
@ -911,7 +931,7 @@ class TestClient(IntegrationTest):
|
||||
def test_list_database_names(self):
|
||||
self.client.pymongo_test.test.insert_one({"dummy": "object"})
|
||||
self.client.pymongo_test_mike.test.insert_one({"dummy": "object"})
|
||||
cmd_docs = self.client.admin.command("listDatabases")["databases"]
|
||||
cmd_docs = (self.client.admin.command("listDatabases"))["databases"]
|
||||
cmd_names = [doc["name"] for doc in cmd_docs]
|
||||
|
||||
db_names = self.client.list_database_names()
|
||||
@ -920,8 +940,10 @@ class TestClient(IntegrationTest):
|
||||
self.assertEqual(db_names, cmd_names)
|
||||
|
||||
def test_drop_database(self):
|
||||
self.assertRaises(TypeError, self.client.drop_database, 5)
|
||||
self.assertRaises(TypeError, self.client.drop_database, None)
|
||||
with self.assertRaises(TypeError):
|
||||
self.client.drop_database(5) # type: ignore[arg-type]
|
||||
with self.assertRaises(TypeError):
|
||||
self.client.drop_database(None) # type: ignore[arg-type]
|
||||
|
||||
self.client.pymongo_test.test.insert_one({"dummy": "object"})
|
||||
self.client.pymongo_test2.test.insert_one({"dummy": "object"})
|
||||
@ -944,7 +966,8 @@ class TestClient(IntegrationTest):
|
||||
test_client = rs_or_single_client()
|
||||
coll = test_client.pymongo_test.bar
|
||||
test_client.close()
|
||||
self.assertRaises(InvalidOperation, coll.count_documents, {})
|
||||
with self.assertRaises(InvalidOperation):
|
||||
coll.count_documents({})
|
||||
|
||||
def test_close_kills_cursors(self):
|
||||
if sys.platform.startswith("java"):
|
||||
@ -961,7 +984,7 @@ class TestClient(IntegrationTest):
|
||||
coll.insert_many([{"i": i} for i in range(docs_inserted)])
|
||||
|
||||
# Open a cursor and leave it open on the server.
|
||||
cursor = coll.find().batch_size(10)
|
||||
cursor = (coll.find()).batch_size(10)
|
||||
self.assertTrue(bool(next(cursor)))
|
||||
self.assertLess(cursor.retrieved, docs_inserted)
|
||||
|
||||
@ -992,7 +1015,8 @@ class TestClient(IntegrationTest):
|
||||
self.assertTrue(client._kill_cursors_executor._stopped)
|
||||
|
||||
# Reusing the closed client should raise an InvalidOperation error.
|
||||
self.assertRaises(InvalidOperation, client.admin.command, "ping")
|
||||
with self.assertRaises(InvalidOperation):
|
||||
client.admin.command("ping")
|
||||
# Thread is still stopped.
|
||||
self.assertTrue(client._kill_cursors_executor._stopped)
|
||||
|
||||
@ -1065,8 +1089,10 @@ class TestClient(IntegrationTest):
|
||||
)
|
||||
|
||||
# Auth with lazy connection.
|
||||
rs_or_single_client_noauth(
|
||||
"mongodb://user:pass@%s:%d/pymongo_test" % (host, port), connect=False
|
||||
(
|
||||
rs_or_single_client_noauth(
|
||||
"mongodb://user:pass@%s:%d/pymongo_test" % (host, port), connect=False
|
||||
)
|
||||
).pymongo_test.test.find_one()
|
||||
|
||||
# Wrong password.
|
||||
@ -1074,7 +1100,8 @@ class TestClient(IntegrationTest):
|
||||
"mongodb://user:wrong@%s:%d/pymongo_test" % (host, port), connect=False
|
||||
)
|
||||
|
||||
self.assertRaises(OperationFailure, bad_client.pymongo_test.test.find_one)
|
||||
with self.assertRaises(OperationFailure):
|
||||
bad_client.pymongo_test.test.find_one()
|
||||
|
||||
@client_context.require_auth
|
||||
def test_username_and_password(self):
|
||||
@ -1093,13 +1120,14 @@ class TestClient(IntegrationTest):
|
||||
c.server_info()
|
||||
|
||||
with self.assertRaises(OperationFailure):
|
||||
rs_or_single_client_noauth(username="ad min", password="foo").server_info()
|
||||
(rs_or_single_client_noauth(username="ad min", password="foo")).server_info()
|
||||
|
||||
@client_context.require_auth
|
||||
@client_context.require_no_fips
|
||||
def test_lazy_auth_raises_operation_failure(self):
|
||||
host = client_context.host
|
||||
lazy_client = rs_or_single_client_noauth(
|
||||
f"mongodb://user:wrong@{client_context.host}/pymongo_test", connect=False
|
||||
f"mongodb://user:wrong@{host}/pymongo_test", connect=False
|
||||
)
|
||||
|
||||
assertRaisesExactly(OperationFailure, lazy_client.test.collection.find_one)
|
||||
@ -1125,11 +1153,10 @@ class TestClient(IntegrationTest):
|
||||
self.assertTrue(mongodb_socket in repr(client))
|
||||
|
||||
# Confirm it fails with a missing socket.
|
||||
self.assertRaises(
|
||||
ConnectionFailure,
|
||||
connected,
|
||||
MongoClient("mongodb://%2Ftmp%2Fnon-existent.sock", serverSelectionTimeoutMS=100),
|
||||
)
|
||||
with self.assertRaises(ConnectionFailure):
|
||||
connected(
|
||||
MongoClient("mongodb://%2Ftmp%2Fnon-existent.sock", serverSelectionTimeoutMS=100),
|
||||
)
|
||||
|
||||
def test_document_class(self):
|
||||
c = self.client
|
||||
@ -1154,27 +1181,30 @@ class TestClient(IntegrationTest):
|
||||
maxIdleTimeMS=10500,
|
||||
serverSelectionTimeoutMS=10500,
|
||||
)
|
||||
self.assertEqual(10.5, get_pool(client).opts.connect_timeout)
|
||||
self.assertEqual(10.5, get_pool(client).opts.socket_timeout)
|
||||
self.assertEqual(10.5, get_pool(client).opts.max_idle_time_seconds)
|
||||
self.assertEqual(10.5, (get_pool(client)).opts.connect_timeout)
|
||||
self.assertEqual(10.5, (get_pool(client)).opts.socket_timeout)
|
||||
self.assertEqual(10.5, (get_pool(client)).opts.max_idle_time_seconds)
|
||||
self.assertEqual(10.5, client.options.pool_options.max_idle_time_seconds)
|
||||
self.assertEqual(10.5, client.options.server_selection_timeout)
|
||||
|
||||
def test_socket_timeout_ms_validation(self):
|
||||
c = rs_or_single_client(socketTimeoutMS=10 * 1000)
|
||||
self.assertEqual(10, get_pool(c).opts.socket_timeout)
|
||||
self.assertEqual(10, (get_pool(c)).opts.socket_timeout)
|
||||
|
||||
c = connected(rs_or_single_client(socketTimeoutMS=None))
|
||||
self.assertEqual(None, get_pool(c).opts.socket_timeout)
|
||||
self.assertEqual(None, (get_pool(c)).opts.socket_timeout)
|
||||
|
||||
c = connected(rs_or_single_client(socketTimeoutMS=0))
|
||||
self.assertEqual(None, get_pool(c).opts.socket_timeout)
|
||||
self.assertEqual(None, (get_pool(c)).opts.socket_timeout)
|
||||
|
||||
self.assertRaises(ValueError, rs_or_single_client, socketTimeoutMS=-1)
|
||||
with self.assertRaises(ValueError):
|
||||
rs_or_single_client(socketTimeoutMS=-1)
|
||||
|
||||
self.assertRaises(ValueError, rs_or_single_client, socketTimeoutMS=1e10)
|
||||
with self.assertRaises(ValueError):
|
||||
rs_or_single_client(socketTimeoutMS=1e10)
|
||||
|
||||
self.assertRaises(ValueError, rs_or_single_client, socketTimeoutMS="foo")
|
||||
with self.assertRaises(ValueError):
|
||||
rs_or_single_client(socketTimeoutMS="foo")
|
||||
|
||||
def test_socket_timeout(self):
|
||||
no_timeout = self.client
|
||||
@ -1189,11 +1219,12 @@ class TestClient(IntegrationTest):
|
||||
where_func = delay(timeout_sec + 1)
|
||||
|
||||
def get_x(db):
|
||||
doc = next(db.test.find().where(where_func))
|
||||
doc = next((db.test.find()).where(where_func))
|
||||
return doc["x"]
|
||||
|
||||
self.assertEqual(1, get_x(no_timeout.pymongo_test))
|
||||
self.assertRaises(NetworkTimeout, get_x, timeout.pymongo_test)
|
||||
with self.assertRaises(NetworkTimeout):
|
||||
get_x(timeout.pymongo_test)
|
||||
|
||||
def test_server_selection_timeout(self):
|
||||
client = MongoClient(serverSelectionTimeoutMS=100, connect=False)
|
||||
@ -1224,7 +1255,7 @@ class TestClient(IntegrationTest):
|
||||
|
||||
def test_waitQueueTimeoutMS(self):
|
||||
client = rs_or_single_client(waitQueueTimeoutMS=2000)
|
||||
self.assertEqual(get_pool(client).opts.wait_queue_timeout, 2)
|
||||
self.assertEqual((get_pool(client)).opts.wait_queue_timeout, 2)
|
||||
|
||||
def test_socketKeepAlive(self):
|
||||
pool = get_pool(self.client)
|
||||
@ -1244,11 +1275,11 @@ class TestClient(IntegrationTest):
|
||||
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
||||
aware.pymongo_test.test.insert_one({"x": now})
|
||||
|
||||
self.assertEqual(None, naive.pymongo_test.test.find_one()["x"].tzinfo)
|
||||
self.assertEqual(utc, aware.pymongo_test.test.find_one()["x"].tzinfo)
|
||||
self.assertEqual(None, (naive.pymongo_test.test.find_one())["x"].tzinfo)
|
||||
self.assertEqual(utc, (aware.pymongo_test.test.find_one())["x"].tzinfo)
|
||||
self.assertEqual(
|
||||
aware.pymongo_test.test.find_one()["x"].replace(tzinfo=None),
|
||||
naive.pymongo_test.test.find_one()["x"],
|
||||
(aware.pymongo_test.test.find_one())["x"].replace(tzinfo=None),
|
||||
(naive.pymongo_test.test.find_one())["x"],
|
||||
)
|
||||
|
||||
@client_context.require_ipv6
|
||||
@ -1282,18 +1313,21 @@ class TestClient(IntegrationTest):
|
||||
|
||||
# The socket used for the previous commands has been returned to the
|
||||
# pool
|
||||
self.assertEqual(1, len(get_pool(client).conns))
|
||||
self.assertEqual(1, len((get_pool(client)).conns))
|
||||
|
||||
with contextlib.closing(client):
|
||||
self.assertEqual("bar", client.pymongo_test.test.find_one()["foo"])
|
||||
with self.assertRaises(InvalidOperation):
|
||||
client.pymongo_test.test.find_one()
|
||||
client = rs_or_single_client()
|
||||
with client as client:
|
||||
self.assertEqual("bar", client.pymongo_test.test.find_one()["foo"])
|
||||
with self.assertRaises(InvalidOperation):
|
||||
client.pymongo_test.test.find_one()
|
||||
# contextlib async support was added in Python 3.10
|
||||
if _IS_SYNC or sys.version_info >= (3, 10):
|
||||
with contextlib.closing(client):
|
||||
self.assertEqual("bar", (client.pymongo_test.test.find_one())["foo"])
|
||||
with self.assertRaises(InvalidOperation):
|
||||
client.pymongo_test.test.find_one()
|
||||
client = rs_or_single_client()
|
||||
with client as client:
|
||||
self.assertEqual("bar", (client.pymongo_test.test.find_one())["foo"])
|
||||
with self.assertRaises(InvalidOperation):
|
||||
client.pymongo_test.test.find_one()
|
||||
|
||||
@client_context.require_sync
|
||||
def test_interrupt_signal(self):
|
||||
if sys.platform.startswith("java"):
|
||||
# We can't figure out how to raise an exception on a thread that's
|
||||
@ -1344,7 +1378,7 @@ class TestClient(IntegrationTest):
|
||||
raised = False
|
||||
try:
|
||||
# Will be interrupted by a KeyboardInterrupt.
|
||||
next(db.foo.find({"$where": where}))
|
||||
next(db.foo.find({"$where": where})) # type: ignore[call-overload]
|
||||
except KeyboardInterrupt:
|
||||
raised = True
|
||||
|
||||
@ -1355,7 +1389,7 @@ class TestClient(IntegrationTest):
|
||||
# Raises AssertionError due to PYTHON-294 -- Mongo's response to
|
||||
# the previous find() is still waiting to be read on the socket,
|
||||
# so the request id's don't match.
|
||||
self.assertEqual({"_id": 1}, next(db.foo.find()))
|
||||
self.assertEqual({"_id": 1}, next(db.foo.find())) # type: ignore[call-overload]
|
||||
finally:
|
||||
if old_signal_handler:
|
||||
signal.signal(signal.SIGALRM, old_signal_handler)
|
||||
@ -1374,7 +1408,8 @@ class TestClient(IntegrationTest):
|
||||
old_conn = next(iter(pool.conns))
|
||||
client.pymongo_test.test.drop()
|
||||
client.pymongo_test.test.insert_one({"_id": "foo"})
|
||||
self.assertRaises(OperationFailure, client.pymongo_test.test.insert_one, {"_id": "foo"})
|
||||
with self.assertRaises(OperationFailure):
|
||||
client.pymongo_test.test.insert_one({"_id": "foo"})
|
||||
|
||||
self.assertEqual(socket_count, len(pool.conns))
|
||||
new_con = next(iter(pool.conns))
|
||||
@ -1392,23 +1427,29 @@ class TestClient(IntegrationTest):
|
||||
client = rs_or_single_client(connect=False, w=0)
|
||||
self.addCleanup(client.close)
|
||||
client.test_lazy_connect_w0.test.insert_one({})
|
||||
wait_until(
|
||||
lambda: client.test_lazy_connect_w0.test.count_documents({}) == 1, "find one document"
|
||||
)
|
||||
|
||||
def predicate():
|
||||
return client.test_lazy_connect_w0.test.count_documents({}) == 1
|
||||
|
||||
wait_until(predicate, "find one document")
|
||||
|
||||
client = rs_or_single_client(connect=False, w=0)
|
||||
self.addCleanup(client.close)
|
||||
client.test_lazy_connect_w0.test.update_one({}, {"$set": {"x": 1}})
|
||||
wait_until(
|
||||
lambda: client.test_lazy_connect_w0.test.find_one().get("x") == 1, "update one document"
|
||||
)
|
||||
|
||||
def predicate():
|
||||
return (client.test_lazy_connect_w0.test.find_one()).get("x") == 1
|
||||
|
||||
wait_until(predicate, "update one document")
|
||||
|
||||
client = rs_or_single_client(connect=False, w=0)
|
||||
self.addCleanup(client.close)
|
||||
client.test_lazy_connect_w0.test.delete_one({})
|
||||
wait_until(
|
||||
lambda: client.test_lazy_connect_w0.test.count_documents({}) == 0, "delete one document"
|
||||
)
|
||||
|
||||
def predicate():
|
||||
return client.test_lazy_connect_w0.test.count_documents({}) == 0
|
||||
|
||||
wait_until(predicate, "delete one document")
|
||||
|
||||
@client_context.require_no_mongos
|
||||
def test_exhaust_network_error(self):
|
||||
@ -1445,12 +1486,13 @@ class TestClient(IntegrationTest):
|
||||
|
||||
# Cause a network error on the actual socket.
|
||||
pool = get_pool(c)
|
||||
socket_info = one(pool.conns)
|
||||
socket_info.conn.close()
|
||||
conn = one(pool.conns)
|
||||
conn.conn.close()
|
||||
|
||||
# Connection.authenticate logs, but gets a socket.error. Should be
|
||||
# reraised as AutoReconnect.
|
||||
self.assertRaises(AutoReconnect, c.test.collection.find_one)
|
||||
with self.assertRaises(AutoReconnect):
|
||||
c.test.collection.find_one()
|
||||
|
||||
# No semaphore leak, the pool is allowed to make a new socket.
|
||||
c.test.collection.find_one()
|
||||
@ -1638,13 +1680,19 @@ class TestClient(IntegrationTest):
|
||||
def stop(self):
|
||||
self.running = False
|
||||
|
||||
def run(self):
|
||||
def _run(self):
|
||||
while self.running:
|
||||
exc = AutoReconnect("mock pool error")
|
||||
ctx = _ErrorContext(exc, 0, pool.gen.get_overall(), False, None)
|
||||
client._topology.handle_error(pool.address, ctx)
|
||||
time.sleep(0.001)
|
||||
|
||||
def run(self):
|
||||
if _IS_SYNC:
|
||||
self._run()
|
||||
else:
|
||||
asyncio.run(self._run())
|
||||
|
||||
t = ResetPoolThread(pool)
|
||||
t.start()
|
||||
|
||||
@ -1755,7 +1803,7 @@ class TestClient(IntegrationTest):
|
||||
{"mode": {"times": 1}, "data": {"closeConnection": True, "failCommands": ["find"]}}
|
||||
):
|
||||
assert client.address is not None
|
||||
expected = "{}:{}: ".format(*client.address)
|
||||
expected = "{}:{}: ".format(*(client.address))
|
||||
with self.assertRaisesRegex(AutoReconnect, expected):
|
||||
client.pymongo_test.test.find_one({})
|
||||
|
||||
@ -1814,6 +1862,7 @@ class TestClient(IntegrationTest):
|
||||
"loadBalanced clients do not run SDAM",
|
||||
)
|
||||
@unittest.skipIf(sys.platform == "win32", "Windows does not support SIGSTOP")
|
||||
@client_context.require_sync
|
||||
def test_sigstop_sigcont(self):
|
||||
test_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
script = os.path.join(test_dir, "sigstop_sigcont.py")
|
||||
@ -1961,7 +2010,8 @@ class TestExhaustCursor(IntegrationTest):
|
||||
SON([("$query", {}), ("$orderby", True)]), cursor_type=CursorType.EXHAUST
|
||||
)
|
||||
|
||||
self.assertRaises(OperationFailure, cursor.next)
|
||||
with self.assertRaises(OperationFailure):
|
||||
cursor.next()
|
||||
self.assertFalse(conn.closed)
|
||||
|
||||
# The socket was checked in and the semaphore was decremented.
|
||||
@ -1998,7 +2048,8 @@ class TestExhaustCursor(IntegrationTest):
|
||||
return message._OpReply.unpack(msg)
|
||||
|
||||
conn.receive_message = receive_message
|
||||
self.assertRaises(OperationFailure, list, cursor)
|
||||
with self.assertRaises(OperationFailure):
|
||||
cursor.to_list()
|
||||
# Unpatch the instance.
|
||||
del conn.receive_message
|
||||
|
||||
@ -2019,7 +2070,8 @@ class TestExhaustCursor(IntegrationTest):
|
||||
conn.conn.close()
|
||||
|
||||
cursor = collection.find(cursor_type=CursorType.EXHAUST)
|
||||
self.assertRaises(ConnectionFailure, cursor.next)
|
||||
with self.assertRaises(ConnectionFailure):
|
||||
cursor.next()
|
||||
self.assertTrue(conn.closed)
|
||||
|
||||
# The socket was closed and the semaphore was decremented.
|
||||
@ -2046,7 +2098,8 @@ class TestExhaustCursor(IntegrationTest):
|
||||
conn.conn.close()
|
||||
|
||||
# A getmore fails.
|
||||
self.assertRaises(ConnectionFailure, list, cursor)
|
||||
with self.assertRaises(ConnectionFailure):
|
||||
cursor.to_list()
|
||||
self.assertTrue(conn.closed)
|
||||
|
||||
wait_until(
|
||||
@ -2057,6 +2110,7 @@ class TestExhaustCursor(IntegrationTest):
|
||||
self.assertNotIn(conn, pool.conns)
|
||||
self.assertEqual(0, pool.requests)
|
||||
|
||||
@client_context.require_sync
|
||||
def test_gevent_task(self):
|
||||
if not gevent_monkey_patched():
|
||||
raise SkipTest("Must be running monkey patched by gevent")
|
||||
@ -2070,6 +2124,7 @@ class TestExhaustCursor(IntegrationTest):
|
||||
task.kill()
|
||||
self.assertTrue(task.dead)
|
||||
|
||||
@client_context.require_sync
|
||||
def test_gevent_timeout(self):
|
||||
if not gevent_monkey_patched():
|
||||
raise SkipTest("Must be running monkey patched by gevent")
|
||||
@ -2101,6 +2156,7 @@ class TestExhaustCursor(IntegrationTest):
|
||||
self.assertIsNone(tt.get())
|
||||
self.assertIsNone(ct.get())
|
||||
|
||||
@client_context.require_sync
|
||||
def test_gevent_timeout_when_creating_connection(self):
|
||||
if not gevent_monkey_patched():
|
||||
raise SkipTest("Must be running monkey patched by gevent")
|
||||
@ -2145,6 +2201,7 @@ class TestClientLazyConnect(IntegrationTest):
|
||||
def _get_client(self):
|
||||
return rs_or_single_client(connect=False)
|
||||
|
||||
@client_context.require_sync
|
||||
def test_insert_one(self):
|
||||
def reset(collection):
|
||||
collection.drop()
|
||||
@ -2157,6 +2214,7 @@ class TestClientLazyConnect(IntegrationTest):
|
||||
|
||||
lazy_client_trial(reset, insert_one, test, self._get_client)
|
||||
|
||||
@client_context.require_sync
|
||||
def test_update_one(self):
|
||||
def reset(collection):
|
||||
collection.drop()
|
||||
@ -2171,6 +2229,7 @@ class TestClientLazyConnect(IntegrationTest):
|
||||
|
||||
lazy_client_trial(reset, update_one, test, self._get_client)
|
||||
|
||||
@client_context.require_sync
|
||||
def test_delete_one(self):
|
||||
def reset(collection):
|
||||
collection.drop()
|
||||
@ -2184,6 +2243,7 @@ class TestClientLazyConnect(IntegrationTest):
|
||||
|
||||
lazy_client_trial(reset, delete_one, test, self._get_client)
|
||||
|
||||
@client_context.require_sync
|
||||
def test_find_one(self):
|
||||
results: list = []
|
||||
|
||||
@ -2203,7 +2263,7 @@ class TestClientLazyConnect(IntegrationTest):
|
||||
|
||||
class TestMongoClientFailover(MockClientTest):
|
||||
def test_discover_primary(self):
|
||||
c = MockClient(
|
||||
c = MockClient.get_mock_client(
|
||||
standalones=[],
|
||||
members=["a:1", "b:2", "c:3"],
|
||||
mongoses=[],
|
||||
@ -2219,13 +2279,17 @@ class TestMongoClientFailover(MockClientTest):
|
||||
# Fail over.
|
||||
c.kill_host("a:1")
|
||||
c.mock_primary = "b:2"
|
||||
wait_until(lambda: c.address == ("b", 2), "wait for server address to be updated")
|
||||
|
||||
def predicate():
|
||||
return (c.address) == ("b", 2)
|
||||
|
||||
wait_until(predicate, "wait for server address to be updated")
|
||||
# a:1 not longer in nodes.
|
||||
self.assertLess(len(c.nodes), 3)
|
||||
|
||||
def test_reconnect(self):
|
||||
# Verify the node list isn't forgotten during a network failure.
|
||||
c = MockClient(
|
||||
c = MockClient.get_mock_client(
|
||||
standalones=[],
|
||||
members=["a:1", "b:2", "c:3"],
|
||||
mongoses=[],
|
||||
@ -2245,12 +2309,13 @@ class TestMongoClientFailover(MockClientTest):
|
||||
|
||||
# MongoClient discovers it's alone. The first attempt raises either
|
||||
# ServerSelectionTimeoutError or AutoReconnect (from
|
||||
# MockPool.get_socket).
|
||||
self.assertRaises(AutoReconnect, c.db.collection.find_one)
|
||||
# AsyncMockPool.get_socket).
|
||||
with self.assertRaises(AutoReconnect):
|
||||
c.db.collection.find_one()
|
||||
|
||||
# But it can reconnect.
|
||||
c.revive_host("a:1")
|
||||
c._get_topology().select_servers(writable_server_selector, _Op.TEST)
|
||||
(c._get_topology()).select_servers(writable_server_selector, _Op.TEST)
|
||||
self.assertEqual(c.address, ("a", 1))
|
||||
|
||||
def _test_network_error(self, operation_callback):
|
||||
@ -2273,7 +2338,7 @@ class TestMongoClientFailover(MockClientTest):
|
||||
# Set host-specific information so we can test whether it is reset.
|
||||
c.set_wire_version_range("a:1", 2, 6)
|
||||
c.set_wire_version_range("b:2", 2, 7)
|
||||
c._get_topology().select_servers(writable_server_selector, _Op.TEST)
|
||||
(c._get_topology()).select_servers(writable_server_selector, _Op.TEST)
|
||||
wait_until(lambda: len(c.nodes) == 2, "connect")
|
||||
|
||||
c.kill_host("a:1")
|
||||
@ -2281,17 +2346,18 @@ class TestMongoClientFailover(MockClientTest):
|
||||
# MongoClient is disconnected from the primary. This raises either
|
||||
# ServerSelectionTimeoutError or AutoReconnect (from
|
||||
# MockPool.get_socket).
|
||||
self.assertRaises(AutoReconnect, operation_callback, c)
|
||||
with self.assertRaises(AutoReconnect):
|
||||
operation_callback(c)
|
||||
|
||||
# The primary's description is reset.
|
||||
server_a = c._get_topology().get_server_by_address(("a", 1))
|
||||
server_a = (c._get_topology()).get_server_by_address(("a", 1))
|
||||
sd_a = server_a.description
|
||||
self.assertEqual(SERVER_TYPE.Unknown, sd_a.server_type)
|
||||
self.assertEqual(0, sd_a.min_wire_version)
|
||||
self.assertEqual(0, sd_a.max_wire_version)
|
||||
|
||||
# ...but not the secondary's.
|
||||
server_b = c._get_topology().get_server_by_address(("b", 2))
|
||||
server_b = (c._get_topology()).get_server_by_address(("b", 2))
|
||||
sd_b = server_b.description
|
||||
self.assertEqual(SERVER_TYPE.RSSecondary, sd_b.server_type)
|
||||
self.assertEqual(2, sd_b.min_wire_version)
|
||||
@ -2332,7 +2398,7 @@ class TestClientPool(MockClientTest):
|
||||
@client_context.require_connection
|
||||
def test_rs_client_does_not_maintain_pool_to_arbiters(self):
|
||||
listener = CMAPListener()
|
||||
c = MockClient(
|
||||
c = MockClient.get_mock_client(
|
||||
standalones=[],
|
||||
members=["a:1", "b:2", "c:3", "d:4"],
|
||||
mongoses=[],
|
||||
@ -2363,7 +2429,7 @@ class TestClientPool(MockClientTest):
|
||||
@client_context.require_connection
|
||||
def test_direct_client_maintains_pool_to_arbiter(self):
|
||||
listener = CMAPListener()
|
||||
c = MockClient(
|
||||
c = MockClient.get_mock_client(
|
||||
standalones=[],
|
||||
members=["a:1", "b:2", "c:3"],
|
||||
mongoses=[],
|
||||
|
||||
@ -20,8 +20,8 @@ import uuid
|
||||
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
from test import IntegrationTest, client_context, unittest
|
||||
from test.utils import connected, rs_or_single_client, single_client
|
||||
from test import IntegrationTest, client_context, connected, unittest
|
||||
from test.utils import rs_or_single_client, single_client
|
||||
|
||||
from bson.binary import PYTHON_LEGACY, STANDARD, Binary, UuidRepresentation
|
||||
from bson.codec_options import CodecOptions
|
||||
|
||||
@ -20,13 +20,12 @@ import sys
|
||||
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
from test import IntegrationTest, unittest
|
||||
from test import IntegrationTest, drop_collections, unittest
|
||||
from test.utils import (
|
||||
SpecTestCreator,
|
||||
camel_to_snake,
|
||||
camel_to_snake_args,
|
||||
camel_to_upper_camel,
|
||||
drop_collections,
|
||||
)
|
||||
|
||||
from pymongo import WriteConcern, operations
|
||||
|
||||
@ -22,9 +22,9 @@ from pymongo.operations import _Op
|
||||
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
from test import MockClientTest, client_context, unittest
|
||||
from test import MockClientTest, client_context, connected, unittest
|
||||
from test.pymongo_mocks import MockClient
|
||||
from test.utils import connected, wait_until
|
||||
from test.utils import wait_until
|
||||
|
||||
from pymongo.errors import AutoReconnect, InvalidOperation
|
||||
from pymongo.server_selectors import writable_server_selector
|
||||
|
||||
@ -22,10 +22,9 @@ from functools import partial
|
||||
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
from test import IntegrationTest, unittest
|
||||
from test import IntegrationTest, connected, unittest
|
||||
from test.utils import (
|
||||
ServerAndTopologyEventListener,
|
||||
connected,
|
||||
single_client,
|
||||
wait_until,
|
||||
)
|
||||
|
||||
@ -26,10 +26,9 @@ from pymongo.operations import _Op
|
||||
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
from test import IntegrationTest, SkipTest, client_context, unittest
|
||||
from test import IntegrationTest, SkipTest, client_context, connected, unittest
|
||||
from test.utils import (
|
||||
OvertCommandListener,
|
||||
connected,
|
||||
one,
|
||||
rs_client,
|
||||
single_client,
|
||||
|
||||
@ -21,13 +21,19 @@ import sys
|
||||
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
from test import HAVE_IPADDRESS, IntegrationTest, SkipTest, client_context, unittest
|
||||
from test import (
|
||||
HAVE_IPADDRESS,
|
||||
IntegrationTest,
|
||||
SkipTest,
|
||||
client_context,
|
||||
connected,
|
||||
remove_all_users,
|
||||
unittest,
|
||||
)
|
||||
from test.utils import (
|
||||
EventListener,
|
||||
cat_files,
|
||||
connected,
|
||||
ignore_deprecations,
|
||||
remove_all_users,
|
||||
)
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
|
||||
@ -621,7 +621,10 @@ async def _async_mongo_client(host, port, authenticate=True, directConnection=No
|
||||
):
|
||||
client_options["username"] = db_user
|
||||
client_options["password"] = db_pwd
|
||||
return AsyncMongoClient(uri, port, **client_options)
|
||||
client = AsyncMongoClient(uri, port, **client_options)
|
||||
if client._options.connect:
|
||||
await client.aconnect()
|
||||
return client
|
||||
|
||||
|
||||
def single_client_noauth(h: Any = None, p: Any = None, **kwargs: Any) -> MongoClient[dict]:
|
||||
@ -843,16 +846,6 @@ def server_started_with_auth(client):
|
||||
return "--auth" in argv or "--keyFile" in argv
|
||||
|
||||
|
||||
def drop_collections(db):
|
||||
# Drop all non-system collections in this database.
|
||||
for coll in db.list_collection_names(filter={"name": {"$regex": r"^(?!system\.)"}}):
|
||||
db.drop_collection(coll)
|
||||
|
||||
|
||||
def remove_all_users(db):
|
||||
db.command("dropAllUsersFromDatabase", 1, writeConcern={"w": client_context.w})
|
||||
|
||||
|
||||
def joinall(threads):
|
||||
"""Join threads with a 5-minute timeout, assert joins succeeded"""
|
||||
for t in threads:
|
||||
@ -860,17 +853,6 @@ def joinall(threads):
|
||||
assert not t.is_alive(), "Thread %s hung" % t
|
||||
|
||||
|
||||
def connected(client):
|
||||
"""Convenience to wait for a newly-constructed client to connect."""
|
||||
with warnings.catch_warnings():
|
||||
# Ignore warning that ping is always routed to primary even
|
||||
# if client's read preference isn't PRIMARY.
|
||||
warnings.simplefilter("ignore", UserWarning)
|
||||
client.admin.command("ping") # Force connection.
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def wait_until(predicate, success_description, timeout=10):
|
||||
"""Wait up to 10 seconds (by default) for predicate to be true.
|
||||
|
||||
@ -957,6 +939,20 @@ def assertRaisesExactly(cls, fn, *args, **kwargs):
|
||||
raise AssertionError("%s not raised" % cls)
|
||||
|
||||
|
||||
async def asyncAssertRaisesExactly(cls, fn, *args, **kwargs):
|
||||
"""
|
||||
Unlike the standard assertRaises, this checks that a function raises a
|
||||
specific class of exception, and not a subclass. E.g., check that
|
||||
MongoClient() raises ConnectionFailure but not its subclass, AutoReconnect.
|
||||
"""
|
||||
try:
|
||||
await fn(*args, **kwargs)
|
||||
except Exception as e:
|
||||
assert e.__class__ == cls, f"got {e.__class__.__name__}, expected {cls.__name__}"
|
||||
else:
|
||||
raise AssertionError("%s not raised" % cls)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _ignore_deprecations():
|
||||
with warnings.catch_warnings():
|
||||
|
||||
@ -72,11 +72,20 @@ replacements = {
|
||||
"addAsyncCleanup": "addCleanup",
|
||||
"async_setup_class": "setup_class",
|
||||
"IsolatedAsyncioTestCase": "TestCase",
|
||||
"AsyncUnitTest": "UnitTest",
|
||||
"AsyncMockClient": "MockClient",
|
||||
"async_get_pool": "get_pool",
|
||||
"async_is_mongos": "is_mongos",
|
||||
"async_rs_or_single_client": "rs_or_single_client",
|
||||
"async_rs_or_single_client_noauth": "rs_or_single_client_noauth",
|
||||
"async_rs_client": "rs_client",
|
||||
"async_single_client": "single_client",
|
||||
"async_from_client": "from_client",
|
||||
"aclosing": "closing",
|
||||
"asyncAssertRaisesExactly": "assertRaisesExactly",
|
||||
"get_async_mock_client": "get_mock_client",
|
||||
"aconnect": "_connect",
|
||||
"aclose": "close",
|
||||
}
|
||||
|
||||
docstring_replacements: dict[tuple[str, str], str] = {
|
||||
@ -131,6 +140,8 @@ sync_gridfs_files = [
|
||||
converted_tests = [
|
||||
"__init__.py",
|
||||
"conftest.py",
|
||||
"pymongo_mocks.py",
|
||||
"test_client.py",
|
||||
"test_collection.py",
|
||||
"test_database.py",
|
||||
]
|
||||
|
||||
212
tox.ini
212
tox.ini
@ -1,212 +0,0 @@
|
||||
[tox]
|
||||
requires =
|
||||
tox>=4
|
||||
|
||||
envlist =
|
||||
# Test using the system Python.
|
||||
test,
|
||||
# Test async tests using the system Python.
|
||||
test-async,
|
||||
# Test using the run-tests Evergreen script.
|
||||
test-eg,
|
||||
# Set up encryption files and services.
|
||||
setup-encryption,
|
||||
# Tear down encryption files and services.
|
||||
teardown-encryption,
|
||||
# Run pre-commit on all files.
|
||||
lint,
|
||||
# Run pre-commit on all files, including stages that require manual fixes.
|
||||
lint-manual,
|
||||
# Typecheck using mypy.
|
||||
typecheck-mypy,
|
||||
# Typecheck using pyright.
|
||||
typecheck-pyright,
|
||||
# Typecheck using pyright strict.
|
||||
typecheck-pyright-strict,
|
||||
# Typecheck all files.
|
||||
typecheck,
|
||||
# Build sphinx docs
|
||||
doc,
|
||||
# Server live sphinx docs
|
||||
doc-serve,
|
||||
# Test sphinx docs
|
||||
doc-test,
|
||||
# Linkcheck sphinx docs
|
||||
linkcheck
|
||||
|
||||
labels = # Use labels and -m instead of -e so that tox -m <label> fails instantly if the label does not exist
|
||||
test = test
|
||||
test-async = test-async
|
||||
test-eg = test-eg
|
||||
setup-encryption = setup-encryption
|
||||
teardown-encryption = teardown-encryption
|
||||
lint = lint
|
||||
lint-manual = lint-manual
|
||||
typecheck-mypy = typecheck-mypy
|
||||
typecheck-pyright = typecheck-pyright
|
||||
typecheck-pyright-strict = typecheck-pyright-strict
|
||||
typecheck = typecheck
|
||||
doc = doc
|
||||
doc-serve = doc-serve
|
||||
doc-test = doc-test
|
||||
linkcheck = linkcheck
|
||||
test-mockupdb = test-mockupdb
|
||||
aws-secrets = aws-secrets
|
||||
|
||||
[testenv]
|
||||
package = editable
|
||||
|
||||
[testenv:.pkg]
|
||||
pass_env =
|
||||
NO_EXT
|
||||
|
||||
[testenv:test]
|
||||
description = run base set of unit tests with no extra functionality
|
||||
extras =
|
||||
test
|
||||
commands =
|
||||
pytest -v --durations=5 --maxfail=10 {posargs}
|
||||
|
||||
[testenv:test-async]
|
||||
description = run base set of async unit tests with no extra functionality
|
||||
extras =
|
||||
test
|
||||
commands =
|
||||
pytest -v --durations=5 --maxfail=10 test/asynchronous/ {posargs}
|
||||
|
||||
[testenv:test-eg]
|
||||
description = run tests using run-tests.sh Evergreen script
|
||||
passenv = *
|
||||
extras = test
|
||||
allowlist_externals =
|
||||
bash
|
||||
commands =
|
||||
bash ./.evergreen/run-tests.sh {posargs}
|
||||
|
||||
[testenv:lint]
|
||||
description = run pre-commit
|
||||
skip_install = true
|
||||
deps =
|
||||
pre-commit
|
||||
commands =
|
||||
pre-commit run --all-files
|
||||
|
||||
[testenv:lint-manual]
|
||||
description = run all pre-commit stages, including those that require manual fixes
|
||||
skip_install = true
|
||||
deps =
|
||||
pre-commit
|
||||
commands =
|
||||
pre-commit run --all-files --hook-stage manual
|
||||
|
||||
[testenv:typecheck-mypy]
|
||||
description = run mypy and pyright to typecheck
|
||||
extras =
|
||||
encryption
|
||||
ocsp
|
||||
zstd
|
||||
aws
|
||||
deps =
|
||||
mypy==1.2.0
|
||||
certifi; platform_system == "win32" or platform_system == "Darwin"
|
||||
typing_extensions
|
||||
commands =
|
||||
mypy --install-types --non-interactive bson gridfs tools pymongo
|
||||
mypy --install-types --non-interactive --config-file mypy_test.ini test
|
||||
mypy --install-types --non-interactive test/test_typing.py test/test_typing_strict.py
|
||||
|
||||
[testenv:typecheck-pyright]
|
||||
description = run pyright to typecheck
|
||||
deps =
|
||||
mypy==1.2.0
|
||||
pyright==1.1.290
|
||||
commands =
|
||||
pyright test/test_typing.py test/test_typing_strict.py
|
||||
|
||||
[testenv:typecheck-pyright-strict]
|
||||
description = run pyright with strict mode to typecheck
|
||||
deps =
|
||||
{[testenv:typecheck-pyright]deps}
|
||||
allowlist_externals=echo
|
||||
commands =
|
||||
echo '{"strict": ["tests/test_typing_strict.py"]}' > pyrightconfig.json
|
||||
pyright test/test_typing_strict.py
|
||||
|
||||
[testenv:typecheck]
|
||||
description = run mypy and pyright to typecheck
|
||||
extras =
|
||||
{[testenv:typecheck-mypy]extras}
|
||||
deps =
|
||||
{[testenv:typecheck-mypy]deps}
|
||||
{[testenv:typecheck-pyright]deps}
|
||||
allowlist_externals=echo
|
||||
commands =
|
||||
python tools/ensure_future_annotations_import.py
|
||||
{[testenv:typecheck-mypy]commands}
|
||||
{[testenv:typecheck-pyright]commands}
|
||||
{[testenv:typecheck-pyright-strict]commands}
|
||||
|
||||
[testenv:doc]
|
||||
description = build sphinx docs
|
||||
deps =
|
||||
-rrequirements/docs.txt
|
||||
commands =
|
||||
sphinx-build -W -b html doc ./doc/_build/html
|
||||
|
||||
[testenv:doc-serve]
|
||||
description = serve sphinx docs
|
||||
deps =
|
||||
{[testenv:doc]deps}
|
||||
sphinx-autobuild
|
||||
commands =
|
||||
sphinx-autobuild -W -b html doc --watch ./pymongo --watch ./bson --watch ./gridfs ./doc/_build/serve
|
||||
|
||||
[testenv:doc-test]
|
||||
description = run sphinx doc tests
|
||||
deps =
|
||||
{[testenv:doc]deps}
|
||||
pytz
|
||||
commands =
|
||||
sphinx-build -E -b doctest doc ./doc/_build/doctest
|
||||
|
||||
[testenv:linkcheck]
|
||||
description = check links of sphinx docs
|
||||
deps =
|
||||
{[testenv:doc]deps}
|
||||
commands =
|
||||
sphinx-build -E -b linkcheck doc ./doc/_build/linkcheck
|
||||
|
||||
[testenv:test-mockupdb]
|
||||
description = run mockupdb tests
|
||||
deps =
|
||||
{[testenv:test]deps}
|
||||
https://github.com/ajdavis/mongo-mockup-db/archive/master.zip
|
||||
extras =
|
||||
{[testenv:test]extras}
|
||||
passenv = *
|
||||
allowlist_externals =
|
||||
{[testenv:test]allowlist_externals}
|
||||
commands =
|
||||
{[testenv:test]commands} ./test/mockupdb
|
||||
|
||||
[testenv:setup-encryption]
|
||||
description = set up encryption assets and servers
|
||||
skip_install = true
|
||||
passenv = *
|
||||
allowlist_externals =
|
||||
bash
|
||||
commands =
|
||||
bash .evergreen/setup-libmongocrypt.sh
|
||||
bash {env:DRIVERS_TOOLS:DRIVERS_TOOLS_var_undefined}/.evergreen/csfle/setup-secrets.sh
|
||||
bash {env:DRIVERS_TOOLS}/.evergreen/csfle/start-servers.sh
|
||||
|
||||
[testenv:teardown-encryption]
|
||||
description = tear down encryption assets and servers
|
||||
skip_install = true
|
||||
passenv = *
|
||||
allowlist_externals =
|
||||
bash
|
||||
rm
|
||||
commands =
|
||||
bash {env:DRIVERS_TOOLS:DRIVERS_TOOLS_var_undefined}/.evergreen/csfle/stop-servers.sh
|
||||
rm -rf libmongocrypt/ libmongocrypt_git/ libmongocrypt.tar.gz mongocryptd.pid
|
||||
Loading…
Reference in New Issue
Block a user