From 632762f64bf8f03a125acafa72a832487ca35220 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 2 Nov 2026 14:06:09 +0300 Subject: [PATCH 1/2] chore: drop Python 3.10 support for 4.3.0 --- .github/workflows/_checks.yml | 12 ++++----- .readthedocs.yaml | 2 +- AGENTS.md | 4 +-- README.md | 5 +++- docs/dev/main-decisions.md | 25 ++++++++++++++++++- docs/index.md | 5 +++- mkdocs.yml | 2 ++ pyproject.toml | 9 +++---- tests/container.py | 4 +-- tests/creators.py | 8 +++--- tests/integrations/fastapi/test_fastapi_di.py | 5 +--- .../faststream/test_faststream_di.py | 4 +-- .../litestar/test_litestar_di_simple.py | 5 +--- tests/providers/test_context_resources.py | 2 +- tests/providers/test_selector.py | 2 +- tests/test_packaging.py | 6 +---- tests/test_typing.py | 3 +-- that_depends/injection.py | 3 +-- 18 files changed, 59 insertions(+), 47 deletions(-) diff --git a/.github/workflows/_checks.yml b/.github/workflows/_checks.yml index 2026d0de..b5e1a6e1 100644 --- a/.github/workflows/_checks.yml +++ b/.github/workflows/_checks.yml @@ -11,8 +11,8 @@ jobs: - uses: astral-sh/setup-uv@v7 with: cache-dependency-glob: "**/pyproject.toml" - - run: uv python install 3.10 - - run: uv python pin 3.10 + - run: uv python install 3.11 + - run: uv python pin 3.11 - run: just install lint-ci - run: just adr-check @@ -22,7 +22,6 @@ jobs: fail-fast: false matrix: python-version: - - "3.10" - "3.11" - "3.12" - "3.13" @@ -53,7 +52,7 @@ jobs: floors: # CI10. Nothing else in CI installs the declared floors: `pytest` resolves highest, and the - # scheduled run does the same. This job installs them exactly on 3.10, and the lowest + # scheduled run does the same. This job installs them exactly on 3.11, and the lowest # versions with a wheel on every other entry. # Skipped on the schedule: the floors do not move, and the scheduled `pytest` already covers # the newest releases, which are what users install. @@ -63,7 +62,6 @@ jobs: fail-fast: false matrix: python-version: - - "3.10" - "3.11" - "3.12" - "3.13" @@ -76,9 +74,9 @@ jobs: - run: uv python install ${{ matrix.python-version }} - run: uv python pin ${{ matrix.python-version }} - run: uv venv - - if: matrix.python-version == '3.10' + - if: matrix.python-version == '3.11' run: uv pip compile pyproject.toml --all-extras --no-deps --resolution lowest-direct -o floors.txt - - if: matrix.python-version != '3.10' + - if: matrix.python-version != '3.11' run: uv pip compile pyproject.toml --all-extras --resolution lowest-direct --no-build -o floors.txt - run: uv pip install -r pyproject.toml --all-extras --group dev --constraints floors.txt --no-build - run: uv run --no-sync pytest . diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 43064be9..970bd4d4 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ version: 2 build: os: "ubuntu-22.04" tools: - python: "3.10" + python: "3.11" python: install: diff --git a/AGENTS.md b/AGENTS.md index 551f414f..61526653 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # AGENTS.md `that-depends` is a typed dependency-injection framework for Python -3.10+. It is the org's most-used package and the only repo here with real external +3.11+. It is the org's most-used package and the only repo here with real external contributor traffic: most merged PRs come from someone other than the maintainer. Assume a human reviews your diff. @@ -118,7 +118,7 @@ from the git tag: publishing triggers on a GitHub release and `just publish` run - `ruff` (`select = ["ALL"]`, with an ignore list) and `mypy` in strict mode are configured in [`pyproject.toml`](pyproject.toml). Two consequences that catch new code: a public class, function or method without a docstring fails lint, and the - package targets Python 3.10, so no 3.11+ syntax. `tests/` is exempt from the + package targets Python 3.11, so no 3.12+ syntax. `tests/` is exempt from the docstring and private-access rules. - Docstrings use the `Args:` / `Returns:` form of `providers/base.py`, adding `Raises:` where a caller needs it (`injection.py`, `providers/selector.py`). State the contract; diff --git a/README.md b/README.md index d0b4183f..2bd09680 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Simple, typed dependency injection framework for Python. Features: - Simple async-first DI framework with IOC-container. -- Python 3.10+ support. +- Python 3.11+ support. - Full coverage by types annotations (mypy in strict mode, pyrefly). - Built-in FastAPI, FastStream and Litestar integrations. - Dependency context management with scopes. @@ -36,6 +36,9 @@ Features: pip install that-depends ``` +Starting with 4.3.0, Python 3.11 or newer is required. Python 3.10 users can install +`that-depends<4.3`. See the [Python support policy](https://github.com/modern-python/that-depends/blob/main/docs/dev/main-decisions.md#python-support-policy). + ## Ecosystem `that-depends` is part of the [`modern-python`](https://github.com/modern-python) family. diff --git a/docs/dev/main-decisions.md b/docs/dev/main-decisions.md index cc387c95..297dbbcf 100644 --- a/docs/dev/main-decisions.md +++ b/docs/dev/main-decisions.md @@ -9,5 +9,28 @@ - no need for `# type: ignore` - no need for `typing.cast` 4. The only runtime dependency is `typing-extensions`: - - it backports `override`, `Self`, `TypeIs` and `TypeVar` defaults to Python 3.10; + - it backports `override` to Python 3.11, and `TypeIs`, `TypeVar` defaults and `deprecated` to Python 3.11 and 3.12; - integrations declare their own imports in extras, and `tests/test_packaging.py` checks both. + +## Python support policy + +Minor releases may drop Python versions that have reached +[upstream end of life](https://devguide.python.org/versions/). A Python support drop +alone does not require a major release. Patch releases retain the minimum Python +version of their minor release series. + +Support drops are announced prominently in the release notes, and package metadata +declares the minimum supported Python version through `Requires-Python`. Installers +such as pip use this metadata to select a compatible release, provided it also +satisfies the application's dependency constraints. Older releases remain available; +continued fixes for an older release series are not guaranteed. + +### Python 3.10 and version 4.3.0 + +**Breaking:** that-depends 4.3.0 requires Python 3.11 or newer. Python 3.10 reached +[end of life on October 1, 2026](https://peps.python.org/pep-0619/). + +Applications staying on Python 3.10 should require `that-depends<4.3`. The 4.2 series +supports Python 3.10; 4.2.1 is the latest compatible release when preparing 4.3.0. +Upgrading to Python 3.11 or newer allows installation of 4.3.0 without changing the +application's use of the that-depends API. diff --git a/docs/index.md b/docs/index.md index fa48f806..26a7302b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,12 +15,15 @@ supports the following: - Dependency injection anywhere - Fully typed and tested - Compatibility with popular frameworks like `FastAPI` and `LiteStar` -- Python 3.10+ support +- Python 3.11+ support --- ## Installation +Starting with 4.3.0, Python 3.11 or newer is required. Python 3.10 users can install +`that-depends<4.3`. See the [Python support policy](dev/main-decisions.md#python-support-policy). + === "pip" ```bash pip install that-depends diff --git a/mkdocs.yml b/mkdocs.yml index 1686ad8a..4fd3bdf7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -140,6 +140,8 @@ plugins: - testing/provider-overriding.md: Overriding providers Experimental features: - experimental/lazy.md: The Lazy provider + Python Support: + - dev/main-decisions.md: Python support policy and compatibility decisions Ecosystem: - ecosystem.md: Related projects in the modern-python organization diff --git a/pyproject.toml b/pyproject.toml index 0711291e..7ef22539 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,13 +5,12 @@ authors = [ { name = "Artur Shiriev", email = "me@shiriev.ru" }, ] readme = "README.md" -requires-python = ">=3.10,<4" +requires-python = ">=3.11,<4" license = "MIT" keywords = ["dependency-injection", "di", "ioc-container", "scopes", "mocks", "python"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -54,7 +53,6 @@ dev = [ "litestar", "faststream[nats]", "pytest-randomly", - "tomli", "pyrefly>=0.61.1", ] @@ -67,7 +65,7 @@ module-name = "that_depends" module-root = "" [tool.mypy] -python_version = "3.10" +python_version = "3.11" strict = true @@ -75,7 +73,6 @@ strict = true fix = true unsafe-fixes = true line-length = 120 -target-version = "py310" extend-exclude = [ "docs", ] @@ -114,5 +111,5 @@ fail_under = 100 exclude_also = ["if typing.TYPE_CHECKING:"] [tool.pyrefly] -python-version = "3.10" +python-version = "3.11" preset = "legacy" diff --git a/tests/container.py b/tests/container.py index 69348979..b006d172 100644 --- a/tests/container.py +++ b/tests/container.py @@ -12,7 +12,7 @@ def create_sync_resource() -> typing.Iterator[dt.datetime]: logger.debug("Resource initiated") try: - yield dt.datetime.now(tz=dt.timezone.utc) + yield dt.datetime.now(tz=dt.UTC) finally: logger.debug("Resource destructed") @@ -20,7 +20,7 @@ def create_sync_resource() -> typing.Iterator[dt.datetime]: async def create_async_resource() -> typing.AsyncIterator[dt.datetime]: logger.debug("Async resource initiated") try: - yield dt.datetime.now(tz=dt.timezone.utc) + yield dt.datetime.now(tz=dt.UTC) finally: logger.debug("Async resource destructed") diff --git a/tests/creators.py b/tests/creators.py index 6c2d5a51..f527d0c9 100644 --- a/tests/creators.py +++ b/tests/creators.py @@ -10,7 +10,7 @@ async def create_async_resource() -> typing.AsyncIterator[dt.datetime]: logger.debug("Async resource initiated") try: - yield dt.datetime.now(tz=dt.timezone.utc) + yield dt.datetime.now(tz=dt.UTC) finally: logger.debug("Async resource destructed") @@ -18,7 +18,7 @@ async def create_async_resource() -> typing.AsyncIterator[dt.datetime]: def create_sync_resource() -> typing.Iterator[dt.datetime]: logger.debug("Resource initiated") try: - yield dt.datetime.now(tz=dt.timezone.utc) + yield dt.datetime.now(tz=dt.UTC) finally: logger.debug("Resource destructed") @@ -26,7 +26,7 @@ def create_sync_resource() -> typing.Iterator[dt.datetime]: class ContextManagerResource(typing.ContextManager[dt.datetime]): def __enter__(self) -> dt.datetime: logger.debug("Resource initiated") - return dt.datetime.now(tz=dt.timezone.utc) + return dt.datetime.now(tz=dt.UTC) def __exit__( self, @@ -40,7 +40,7 @@ def __exit__( class AsyncContextManagerResource(typing.AsyncContextManager[dt.datetime]): async def __aenter__(self) -> dt.datetime: logger.debug("Async resource initiated") - return dt.datetime.now(tz=dt.timezone.utc) + return dt.datetime.now(tz=dt.UTC) async def __aexit__( self, diff --git a/tests/integrations/fastapi/test_fastapi_di.py b/tests/integrations/fastapi/test_fastapi_di.py index 6c7cc5c5..3b34e038 100644 --- a/tests/integrations/fastapi/test_fastapi_di.py +++ b/tests/integrations/fastapi/test_fastapi_di.py @@ -56,7 +56,4 @@ def fastapi_client(fastapi_app: fastapi.FastAPI) -> TestClient: async def test_read_main(fastapi_client: TestClient) -> None: response = fastapi_client.get("/") assert response.status_code == status.HTTP_200_OK - assert ( - dt.datetime.fromisoformat(response.json().replace("Z", "+00:00")) - == await container.DIContainer.async_resource() - ) + assert dt.datetime.fromisoformat(response.json()) == await container.DIContainer.async_resource() diff --git a/tests/integrations/faststream/test_faststream_di.py b/tests/integrations/faststream/test_faststream_di.py index 4e41affe..82d07c46 100644 --- a/tests/integrations/faststream/test_faststream_di.py +++ b/tests/integrations/faststream/test_faststream_di.py @@ -43,6 +43,4 @@ async def test_read_main() -> None: result = await br.request(None, TEST_SUBJECT) result_str = typing.cast(str, await result.decode()) - assert ( - dt.datetime.fromisoformat(result_str.replace("Z", "+00:00")) == await container.DIContainer.async_resource() - ) + assert dt.datetime.fromisoformat(result_str) == await container.DIContainer.async_resource() diff --git a/tests/integrations/litestar/test_litestar_di_simple.py b/tests/integrations/litestar/test_litestar_di_simple.py index 4b0d3752..3f1fa05b 100644 --- a/tests/integrations/litestar/test_litestar_di_simple.py +++ b/tests/integrations/litestar/test_litestar_di_simple.py @@ -20,7 +20,4 @@ async def test_litestar_di() -> None: with TestClient(app=app) as client: response = client.get("/") assert response.status_code == HTTP_200_OK, response.text - assert ( - dt.datetime.fromisoformat(response.json().replace("Z", "+00:00")) - == await container.DIContainer.async_resource() - ) + assert dt.datetime.fromisoformat(response.json()) == await container.DIContainer.async_resource() diff --git a/tests/providers/test_context_resources.py b/tests/providers/test_context_resources.py index 3d32be9d..4dc8ee20 100644 --- a/tests/providers/test_context_resources.py +++ b/tests/providers/test_context_resources.py @@ -159,7 +159,7 @@ async def test_context_resource_included_context( async def test_context_resources_overriding(context_resource: providers.ContextResource[str]) -> None: - context_resource_mock = dt.datetime.now(tz=dt.timezone.utc) + context_resource_mock = dt.datetime.now(tz=dt.UTC) context_resource.override_sync(context_resource_mock) context_resource_result = await context_resource() diff --git a/tests/providers/test_selector.py b/tests/providers/test_selector.py index 60369964..cfb364b5 100644 --- a/tests/providers/test_selector.py +++ b/tests/providers/test_selector.py @@ -64,7 +64,7 @@ async def test_selector_provider_sync_missing() -> None: async def test_selector_provider_overriding() -> None: - now = dt.datetime.now(tz=dt.timezone.utc) + now = dt.datetime.now(tz=dt.UTC) DIContainer.selector.override_sync(now) selected_async = await DIContainer.selector() selected_sync = DIContainer.selector.resolve_sync() diff --git a/tests/test_packaging.py b/tests/test_packaging.py index ce521263..3583d780 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -2,15 +2,11 @@ import pathlib import re import sys +import tomllib import that_depends -if sys.version_info >= (3, 11): # pragma: no cover - import tomllib -else: # pragma: no cover - import tomli as tomllib - _PACKAGE_DIR = pathlib.Path(that_depends.__file__).parent _INTEGRATIONS_DIR = _PACKAGE_DIR / "integrations" _PROJECT = tomllib.loads((_PACKAGE_DIR.parent / "pyproject.toml").read_text())["project"] diff --git a/tests/test_typing.py b/tests/test_typing.py index 01e0e96d..8aadc42c 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -1,6 +1,5 @@ import typing - -from typing_extensions import assert_type +from typing import assert_type from that_depends import Provide, providers from that_depends.experimental.providers import LazyProvider diff --git a/that_depends/injection.py b/that_depends/injection.py index 75e832be..91d5e40b 100644 --- a/that_depends/injection.py +++ b/that_depends/injection.py @@ -5,8 +5,7 @@ import warnings from contextlib import AsyncExitStack, ExitStack from types import TracebackType - -from typing_extensions import Self +from typing import Self from that_depends.container import BaseContainer from that_depends.exceptions import TypeNotBoundError From 47b52cbe5e0e1f60e8c4f5b5e4342bd13fb8fd94 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 2 Nov 2026 14:17:26 +0300 Subject: [PATCH 2/2] docs: remove release migration note from README --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 2bd09680..7015ebd7 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,6 @@ Features: pip install that-depends ``` -Starting with 4.3.0, Python 3.11 or newer is required. Python 3.10 users can install -`that-depends<4.3`. See the [Python support policy](https://github.com/modern-python/that-depends/blob/main/docs/dev/main-decisions.md#python-support-policy). - ## Ecosystem `that-depends` is part of the [`modern-python`](https://github.com/modern-python) family.