diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3ed1f0d9b..0c1f2f6d4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3 + uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -63,6 +63,6 @@ jobs: pip install -e . - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3 + uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 40a497480..0ed23b9d8 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -14,6 +14,9 @@ defaults: run: shell: bash -eux {0} +permissions: + contents: read + jobs: static: @@ -163,6 +166,36 @@ jobs: run: | just typing + integration_tests: + runs-on: ubuntu-latest + name: Integration Tests + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Install uv + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v5 + with: + enable-cache: true + python-version: "3.10" + - name: Install just + run: uv tool install rust-just + - name: Install dependencies + run: | + just install + - id: setup-mongodb + uses: mongodb-labs/drivers-evergreen-tools@master + - name: Run tests + run: | + just integration-tests + - id: setup-mongodb-ssl + uses: mongodb-labs/drivers-evergreen-tools@master + with: + ssl: true + - name: Run tests + run: | + just integration-tests + make_sdist: runs-on: ubuntu-latest name: "Make an sdist" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4f7b5581..a8881db9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -411,6 +411,14 @@ a use the ticket number as the "reason" parameter to the decorator, e.g. `@flaky When running tests locally (not in CI), the `flaky` decorator will be disabled unless `ENABLE_FLAKY` is set. To disable the `flaky` decorator in CI, you can use `evergreen patch --param DISABLE_FLAKY=1`. +## Integration Tests + +The `integration_tests` directory has a set of scripts that verify the usage of PyMongo with downstream packages or frameworks. See the [README](./integration_tests/README.md) for more information. + +To run the tests, use `just integration_tests`. + +The tests should be able to run with and without SSL enabled. + ## Specification Tests The MongoDB [specifications repository](https://github.com/mongodb/specifications) diff --git a/integration_tests/README.md b/integration_tests/README.md new file mode 100644 index 000000000..fb64a9066 --- /dev/null +++ b/integration_tests/README.md @@ -0,0 +1,42 @@ +# Integration Tests + +A set of tests that verify the usage of PyMongo with downstream packages or frameworks. + +Each test uses [PEP 723 inline metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata/) and can be run using `pipx` or `uv`. + +The `run.sh` convenience script can be used to run all of the files using `uv`. + +Here is an example header for the script with the inline dependencies: + +```python +# /// script +# dependencies = [ +# "uvloop>=0.18" +# ] +# requires-python = ">=3.10" +# /// +``` + +Here is an example of using the test helper function to create a configured client for the test: + + +```python +import asyncio +import sys +from pathlib import Path + +# Use pymongo from parent directory. +root = Path(__file__).parent.parent +sys.path.insert(0, str(root)) + +from test.asynchronous import async_simple_test_client # noqa: E402 + + +async def main(): + async with async_simple_test_client() as client: + result = await client.admin.command("ping") + assert result["ok"] + + +asyncio.run(main()) +``` diff --git a/integration_tests/run.sh b/integration_tests/run.sh new file mode 100755 index 000000000..051e2b8a7 --- /dev/null +++ b/integration_tests/run.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Run all of the integration test files using `uv run`. +set -eu + +for file in integration_tests/test_*.py ; do + echo "-----------------" + echo "Running $file..." + uv run $file + echo "Running $file...done." + echo "-----------------" +done diff --git a/integration_tests/test_uv_loop.py b/integration_tests/test_uv_loop.py new file mode 100644 index 000000000..88a3ad73a --- /dev/null +++ b/integration_tests/test_uv_loop.py @@ -0,0 +1,27 @@ +# /// script +# dependencies = [ +# "uvloop>=0.18" +# ] +# requires-python = ">=3.10" +# /// +from __future__ import annotations + +import sys +from pathlib import Path + +import uvloop + +# Use pymongo from parent directory. +root = Path(__file__).parent.parent +sys.path.insert(0, str(root)) + +from test.asynchronous import async_simple_test_client # noqa: E402 + + +async def main(): + async with async_simple_test_client() as client: + result = await client.admin.command("ping") + assert result["ok"] + + +uvloop.run(main()) diff --git a/justfile b/justfile index 9b6cce62c..f23534616 100644 --- a/justfile +++ b/justfile @@ -72,6 +72,10 @@ setup-tests *args="": teardown-tests: bash .evergreen/scripts/teardown-tests.sh +[group('test')] +integration-tests: + bash integration_tests/run.sh + [group('server')] run-server *args="": bash .evergreen/scripts/run-server.sh {{args}} diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index 0130f0e8b..9c4d9a9d5 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -19,7 +19,7 @@ import ipaddress import random from typing import TYPE_CHECKING, Any, Optional, Union -from pymongo.common import CONNECT_TIMEOUT, check_for_min_version +from pymongo.common import CONNECT_TIMEOUT from pymongo.errors import ConfigurationError if TYPE_CHECKING: @@ -32,14 +32,6 @@ def _have_dnspython() -> bool: try: import dns # noqa: F401 - dns_version, required_version, is_valid = check_for_min_version("dnspython") - if not is_valid: - raise RuntimeError( - f"pymongo requires dnspython>={required_version}, " - f"found version {dns_version}. " - "Install a compatible version with pip" - ) - return True except ImportError: return False @@ -79,8 +71,6 @@ class _SrvResolver: srv_service_name: str, srv_max_hosts: int = 0, ): - # Ensure the version of dnspython is compatible. - _have_dnspython() self.__fqdn = fqdn self.__srv = srv_service_name self.__connect_timeout = connect_timeout or CONNECT_TIMEOUT diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index e3e208e5c..480231069 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -19,7 +19,7 @@ import ipaddress import random from typing import TYPE_CHECKING, Any, Optional, Union -from pymongo.common import CONNECT_TIMEOUT, check_for_min_version +from pymongo.common import CONNECT_TIMEOUT from pymongo.errors import ConfigurationError if TYPE_CHECKING: @@ -32,14 +32,6 @@ def _have_dnspython() -> bool: try: import dns # noqa: F401 - dns_version, required_version, is_valid = check_for_min_version("dnspython") - if not is_valid: - raise RuntimeError( - f"pymongo requires dnspython>={required_version}, " - f"found version {dns_version}. " - "Install a compatible version with pip" - ) - return True except ImportError: return False @@ -79,8 +71,6 @@ class _SrvResolver: srv_service_name: str, srv_max_hosts: int = 0, ): - # Ensure the version of dnspython is compatible. - _have_dnspython() self.__fqdn = fqdn self.__srv = srv_service_name self.__connect_timeout = connect_timeout or CONNECT_TIMEOUT diff --git a/test/__init__.py b/test/__init__.py index f3b66c20a..1ee2c283d 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1227,6 +1227,13 @@ def teardown(): print_running_clients() +@contextmanager +def simple_test_client(): + client_context.init() + yield client_context.client + client_context.client.close() + + def test_cases(suite): """Iterator over all TestCases within a TestSuite.""" for suite_or_case in suite._tests: diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 7a6a23ed2..78d0576ad 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -1243,6 +1243,13 @@ async def async_teardown(): print_running_clients() +@asynccontextmanager +async def async_simple_test_client(): + await async_client_context.init() + yield async_client_context.client + await async_client_context.client.close() + + def test_cases(suite): """Iterator over all TestCases within a TestSuite.""" for suite_or_case in suite._tests: diff --git a/tools/synchro.py b/tools/synchro.py index a4190529c..e3d483550 100644 --- a/tools/synchro.py +++ b/tools/synchro.py @@ -131,6 +131,7 @@ replacements = { "async_create_barrier": "create_barrier", "async_barrier_wait": "barrier_wait", "async_joinall": "joinall", + "async_simple_test_client": "simple_test_client", "_async_create_connection": "_create_connection", "pymongo.asynchronous.srv_resolver._SrvResolver.get_hosts": "pymongo.synchronous.srv_resolver._SrvResolver.get_hosts", "dns.asyncresolver.resolve": "dns.resolver.resolve",