From 41c8ec0b65f16fba7aaff788d52828423a9e3d97 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 3 Oct 2026 12:03:09 +0300 Subject: [PATCH 1/3] fix: declare typing-extensions and packaging as runtime dependencies that_depends imports typing_extensions at module level in most modules, and the faststream integration imports packaging, but neither was declared, so a clean install failed on import. Add a test that every top-level third-party import is a declared requirement, and drop the zero-dependencies claim. --- AGENTS.md | 2 +- README.md | 1 - docs/dev/main-decisions.md | 3 +++ pyproject.toml | 7 ++++-- tests/test_packaging.py | 48 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 tests/test_packaging.py diff --git a/AGENTS.md b/AGENTS.md index a8c10695..df06b1ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md -`that-depends` is a zero-dependency, typed dependency-injection framework for Python +`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 contributor traffic: most merged PRs come from someone other than the maintainer. Assume a human reviews your diff. diff --git a/README.md b/README.md index 72d99bc3..886eda17 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ It is production-ready and gives you the following: - Dependency context management with scopes. - Overriding dependencies for tests. - Injecting dependencies in functions and coroutines without wiring. -- Package with zero dependencies. ### Installation diff --git a/docs/dev/main-decisions.md b/docs/dev/main-decisions.md index c0549975..cc387c95 100644 --- a/docs/dev/main-decisions.md +++ b/docs/dev/main-decisions.md @@ -8,3 +8,6 @@ 3. Focus on maximum compatibility with mypy: - 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; + - integrations declare their own imports in extras, and `tests/test_packaging.py` checks both. diff --git a/pyproject.toml b/pyproject.toml index a37d3df9..2a5bd805 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,13 +20,17 @@ classifiers = [ "Topic :: Software Development :: Libraries", ] version = "0" +dependencies = [ + "typing-extensions>=4.10", +] [project.optional-dependencies] fastapi = [ "fastapi>=0.115.3", ] faststream = [ - "faststream>=0.3.14" + "faststream>=0.3.14", + "packaging", ] [project.urls] @@ -45,7 +49,6 @@ dev = [ "pytest-repeat", "ruff", "mypy", - "typing-extensions", "pre-commit", "litestar", "faststream[nats]", diff --git a/tests/test_packaging.py b/tests/test_packaging.py new file mode 100644 index 00000000..d8c17d8d --- /dev/null +++ b/tests/test_packaging.py @@ -0,0 +1,48 @@ +import ast +import importlib.metadata +import pathlib +import re +import sys + +import that_depends + + +_PACKAGE_DIR = pathlib.Path(that_depends.__file__).parent +_INTEGRATIONS_DIR = _PACKAGE_DIR / "integrations" + + +def _top_level_imports(path: pathlib.Path) -> set[str]: + roots: set[str] = set() + for node in ast.parse(path.read_text()).body: + if isinstance(node, ast.Import): + roots.update(alias.name.split(".")[0] for alias in node.names) + elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module: + roots.add(node.module.split(".")[0]) + return roots + + +def _normalize(name: str) -> str: + return re.sub(r"[-_.]+", "-", name).lower() + + +def _third_party_imports(paths: list[pathlib.Path]) -> set[str]: + imported = set().union(*(_top_level_imports(path) for path in paths)) + return {_normalize(name) for name in imported if name not in sys.stdlib_module_names and name != "that_depends"} + + +def _declared(*, include_extras: bool) -> set[str]: + return { + _normalize(re.split(r"[\s;<>=!~\[]", requirement, maxsplit=1)[0]) + for requirement in importlib.metadata.requires("that-depends") or [] + if include_extras or "extra ==" not in requirement + } + + +def test_core_runtime_imports_are_unconditional_dependencies() -> None: + core = [path for path in _PACKAGE_DIR.rglob("*.py") if _INTEGRATIONS_DIR not in path.parents] + assert _third_party_imports(core) <= _declared(include_extras=False) + + +def test_integration_runtime_imports_are_declared_dependencies() -> None: + integrations = list(_INTEGRATIONS_DIR.rglob("*.py")) + assert _third_party_imports(integrations) <= _declared(include_extras=True) From 5cbf9e53221fa9346812186ff66956fe846ed42c Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 3 Oct 2026 12:23:12 +0300 Subject: [PATCH 2/3] fix: raise dependency floors to versions the floors job can install Declaring typing-extensions changed the lowest-direct resolution in the floors job. That exposed faststream>=0.3.14 as a false floor: on 0.3.14 and 0.4.x DIContextMiddleware cannot be constructed, and the suite needs 0.5.19. On Python 3.14 the floors job only installs wheels, so pydantic must be 2.12+, which needs typing-extensions>=4.14.1. The packaging test now reads pyproject.toml, because the floors job does not install the project itself. --- pyproject.toml | 4 ++-- tests/test_packaging.py | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2a5bd805..aaa6f663 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ classifiers = [ ] version = "0" dependencies = [ - "typing-extensions>=4.10", + "typing-extensions>=4.14.1", ] [project.optional-dependencies] @@ -29,7 +29,7 @@ fastapi = [ "fastapi>=0.115.3", ] faststream = [ - "faststream>=0.3.14", + "faststream>=0.5.19", "packaging", ] diff --git a/tests/test_packaging.py b/tests/test_packaging.py index d8c17d8d..f057b878 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -1,14 +1,18 @@ import ast -import importlib.metadata import pathlib import re import sys +import pytest + import that_depends +tomllib = pytest.importorskip("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"] def _top_level_imports(path: pathlib.Path) -> set[str]: @@ -31,11 +35,10 @@ def _third_party_imports(paths: list[pathlib.Path]) -> set[str]: def _declared(*, include_extras: bool) -> set[str]: - return { - _normalize(re.split(r"[\s;<>=!~\[]", requirement, maxsplit=1)[0]) - for requirement in importlib.metadata.requires("that-depends") or [] - if include_extras or "extra ==" not in requirement - } + requirements = list(_PROJECT.get("dependencies", [])) + if include_extras: + requirements += [req for extra in _PROJECT.get("optional-dependencies", {}).values() for req in extra] + return {_normalize(re.split(r"[\s;<>=!~\[]", requirement, maxsplit=1)[0]) for requirement in requirements} def test_core_runtime_imports_are_unconditional_dependencies() -> None: From 9abd22e46c5a6dc2bf545bfebe58c5c1d10a5026 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 3 Oct 2026 12:35:39 +0300 Subject: [PATCH 3/3] test: parse pyproject with tomli on 3.10 so the packaging test runs everywhere Skipping it on 3.10 dropped coverage below the 100% gate. --- pyproject.toml | 1 + tests/test_packaging.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aaa6f663..7f9dd40c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ dev = [ "litestar", "faststream[nats]", "pytest-randomly", + "tomli", "pyrefly>=0.61.1", ] diff --git a/tests/test_packaging.py b/tests/test_packaging.py index f057b878..ce521263 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -3,12 +3,13 @@ import re import sys -import pytest - import that_depends -tomllib = pytest.importorskip("tomllib") +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"