Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions .github/workflows/_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -22,7 +22,6 @@ jobs:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
Expand Down Expand Up @@ -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.
Expand All @@ -63,7 +62,6 @@ jobs:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
Expand All @@ -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 .
Expand Down
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 2
build:
os: "ubuntu-22.04"
tools:
python: "3.10"
python: "3.11"

python:
install:
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 24 additions & 1 deletion docs/dev/main-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
5 changes: 4 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 3 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -54,7 +53,6 @@ dev = [
"litestar",
"faststream[nats]",
"pytest-randomly",
"tomli",
"pyrefly>=0.61.1",
]

Expand All @@ -67,15 +65,14 @@ module-name = "that_depends"
module-root = ""

[tool.mypy]
python_version = "3.10"
python_version = "3.11"
strict = true


[tool.ruff]
fix = true
unsafe-fixes = true
line-length = 120
target-version = "py310"
extend-exclude = [
"docs",
]
Expand Down Expand Up @@ -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"
4 changes: 2 additions & 2 deletions tests/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
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")


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")

Expand Down
8 changes: 4 additions & 4 deletions tests/creators.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,23 @@
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")


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")


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,
Expand All @@ -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,
Expand Down
5 changes: 1 addition & 4 deletions tests/integrations/fastapi/test_fastapi_di.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
4 changes: 1 addition & 3 deletions tests/integrations/faststream/test_faststream_di.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
5 changes: 1 addition & 4 deletions tests/integrations/litestar/test_litestar_di_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
2 changes: 1 addition & 1 deletion tests/providers/test_context_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion tests/providers/test_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
6 changes: 1 addition & 5 deletions tests/test_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
3 changes: 1 addition & 2 deletions tests/test_typing.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 1 addition & 2 deletions that_depends/injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading