From 6685c5a14154d10f6331151da15f64f073fa3bfc Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 10 Mar 2025 20:35:37 +0000 Subject: [PATCH 01/20] Fix infinite loop with Closing+ConfigurationOption --- src/dependency_injector/wiring.py | 22 +++---------------- .../wiringstringids/resourceclosing.py | 5 +++-- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 9de6f823..a3a87e20 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -628,21 +628,6 @@ def _fetch_reference_injections( # noqa: C901 return injections, closing -def _locate_dependent_closing_args( - provider: providers.Provider, closing_deps: Dict[str, providers.Provider] -) -> Dict[str, providers.Provider]: - for arg in [ - *getattr(provider, "args", []), - *getattr(provider, "kwargs", {}).values(), - ]: - if not isinstance(arg, providers.Provider): - continue - if isinstance(arg, providers.Resource): - closing_deps[str(id(arg))] = arg - - _locate_dependent_closing_args(arg, closing_deps) - - def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> None: patched_callable = _patched_registry.get_callable(fn) if patched_callable is None: @@ -664,10 +649,9 @@ def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> Non if injection in patched_callable.reference_closing: patched_callable.add_closing(injection, provider) - deps = {} - _locate_dependent_closing_args(provider, deps) - for key, dep in deps.items(): - patched_callable.add_closing(key, dep) + + for resource in provider.traverse(types=[providers.Resource]): + patched_callable.add_closing(str(id(resource)), resource) def _unbind_injections(fn: Callable[..., Any]) -> None: diff --git a/tests/unit/samples/wiringstringids/resourceclosing.py b/tests/unit/samples/wiringstringids/resourceclosing.py index c4d1f20f..5a3d2ba4 100644 --- a/tests/unit/samples/wiringstringids/resourceclosing.py +++ b/tests/unit/samples/wiringstringids/resourceclosing.py @@ -59,12 +59,13 @@ def init_service(counter: Counter, _list: List[int], _dict: Dict[str, int]): class Container(containers.DeclarativeContainer): + config = providers.Configuration(default={"a": 1, "b": 4}) counter = providers.Singleton(Counter) _list = providers.List( - providers.Callable(lambda a: a, a=1), providers.Callable(lambda b: b, 2) + providers.Callable(lambda a: a, a=config.a), providers.Callable(lambda b: b, 2) ) _dict = providers.Dict( - a=providers.Callable(lambda a: a, a=3), b=providers.Callable(lambda b: b, 4) + a=providers.Callable(lambda a: a, a=3), b=providers.Callable(lambda b: b, config.b) ) service = providers.Resource(init_service, counter, _list, _dict=_dict) service2 = providers.Resource(init_service, counter, _list, _dict=_dict) From 35bfafdfe2d59d23e0c4f20e2e731ad53255bc84 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 7 Apr 2025 14:57:13 +0300 Subject: [PATCH 02/20] Move pytest config to pyproject.toml (#876) * Move pytest config to pyproject.toml * Fix pytest warning --- Makefile | 2 +- pyproject.toml | 14 ++++++++++++++ tests/.configs/pytest.ini | 13 ------------- tests/unit/ext/test_flask_py2_py3.py | 4 ++-- tox.ini | 8 ++++---- 5 files changed, 21 insertions(+), 20 deletions(-) delete mode 100644 tests/.configs/pytest.ini diff --git a/Makefile b/Makefile index 84d0ef86..29e4086f 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ uninstall: test: # Unit tests with coverage report coverage erase - coverage run -m pytest -c tests/.configs/pytest.ini + coverage run -m pytest coverage report coverage html diff --git a/pyproject.toml b/pyproject.toml index eba17764..794b36d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,3 +99,17 @@ ignore = ["tests"] [tool.pylint.design] min-public-methods = 0 max-public-methods = 30 + +[tool.pytest.ini_options] +testpaths = ["tests/unit/"] +asyncio_mode = "auto" +markers = [ + "pydantic: Tests with Pydantic as a dependency", +] +filterwarnings = [ + "ignore:Module \"dependency_injector.ext.aiohttp\" is deprecated since version 4\\.0\\.0:DeprecationWarning", + "ignore:Module \"dependency_injector.ext.flask\" is deprecated since version 4\\.0\\.0:DeprecationWarning", + "ignore:Please use \\`.*?\\` from the \\`scipy.*?\\`(.*?)namespace is deprecated\\.:DeprecationWarning", + "ignore:Please import \\`.*?\\` from the \\`scipy(.*?)\\` namespace(.*):DeprecationWarning", + "ignore:\\`scipy(.*?)\\` is deprecated(.*):DeprecationWarning", +] diff --git a/tests/.configs/pytest.ini b/tests/.configs/pytest.ini deleted file mode 100644 index ea92be96..00000000 --- a/tests/.configs/pytest.ini +++ /dev/null @@ -1,13 +0,0 @@ -[pytest] -testpaths = tests/unit/ -python_files = test_*_py3*.py -asyncio_mode = auto -markers = - pydantic: Tests with Pydantic as a dependency -filterwarnings = - ignore:Module \"dependency_injector.ext.aiohttp\" is deprecated since version 4\.0\.0:DeprecationWarning - ignore:Module \"dependency_injector.ext.flask\" is deprecated since version 4\.0\.0:DeprecationWarning - ignore:Please use \`.*?\` from the \`scipy.*?\`(.*?)namespace is deprecated\.:DeprecationWarning - ignore:Please import \`.*?\` from the \`scipy(.*?)\` namespace(.*):DeprecationWarning - ignore:\`scipy(.*?)\` is deprecated(.*):DeprecationWarning - ignore:ssl\.PROTOCOL_TLS is deprecated:DeprecationWarning:botocore.* diff --git a/tests/unit/ext/test_flask_py2_py3.py b/tests/unit/ext/test_flask_py2_py3.py index e64de165..3e79e067 100644 --- a/tests/unit/ext/test_flask_py2_py3.py +++ b/tests/unit/ext/test_flask_py2_py3.py @@ -11,7 +11,7 @@ def index(): return "Hello World!" -def test(): +def _test(): return "Test!" @@ -25,7 +25,7 @@ class ApplicationContainer(containers.DeclarativeContainer): app = flask.Application(Flask, __name__) index_view = flask.View(index) - test_view = flask.View(test) + test_view = flask.View(_test) test_class_view = flask.ClassBasedView(Test) diff --git a/tox.ini b/tox.ini index 54f99e57..b37bfeaa 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ deps= werkzeug extras= yaml -commands = pytest -c tests/.configs/pytest.ini +commands = pytest python_files = test_*_py3*.py setenv = COVERAGE_RCFILE = pyproject.toml @@ -45,7 +45,7 @@ deps = boto3 mypy_boto3_s3 werkzeug -commands = pytest -c tests/.configs/pytest.ini -m pydantic +commands = pytest -m pydantic [testenv:coveralls] passenv = GITHUB_*, COVERALLS_*, DEPENDENCY_INJECTOR_* @@ -57,7 +57,7 @@ deps= coveralls>=4 commands= coverage erase - coverage run -m pytest -c tests/.configs/pytest.ini + coverage run -m pytest coverage report coveralls @@ -74,7 +74,7 @@ deps= mypy_boto3_s3 extras= yaml -commands = pytest -c tests/.configs/pytest-py35.ini +commands = pytest [testenv:pylint] From 4ae79ac21f0d18e029add43a6b9ec5199dfdbadd Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 7 Apr 2025 14:57:45 +0300 Subject: [PATCH 03/20] Remove unused root property from ConfigurationOption (#875) fixes #874 --- src/dependency_injector/providers.pyx | 3 +-- src/dependency_injector/wiring.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/dependency_injector/providers.pyx b/src/dependency_injector/providers.pyx index 73c5cbe1..27fda027 100644 --- a/src/dependency_injector/providers.pyx +++ b/src/dependency_injector/providers.pyx @@ -1592,8 +1592,7 @@ cdef class ConfigurationOption(Provider): segment() if is_provider(segment) else segment for segment in self._name ) - @property - def root(self): + def _get_root(self): return self._root def get_name(self): diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index a3a87e20..a67cf76b 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -314,7 +314,7 @@ class ProvidersMap: original: providers.ConfigurationOption, as_: Any = None, ) -> Optional[providers.Provider]: - original_root = original.root + original_root = original._get_root() new = self._resolve_provider(original_root) if new is None: return None From 9a08bfcede003d5e0f8cf7cfdb11a1b3db7ceef9 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 18 May 2025 12:06:33 +0300 Subject: [PATCH 04/20] Drop Python 3.7 support (#885) --- .github/workflows/tests-and-linters.yml | 18 +----------------- pyproject.toml | 3 +-- src/dependency_injector/providers.pyx | 11 +++-------- .../resource/test_async_resource_py35.py | 1 - tox.ini | 2 +- 5 files changed, 6 insertions(+), 29 deletions(-) diff --git a/.github/workflows/tests-and-linters.yml b/.github/workflows/tests-and-linters.yml index a924c5e7..f43a2db8 100644 --- a/.github/workflows/tests-and-linters.yml +++ b/.github/workflows/tests-and-linters.yml @@ -4,28 +4,12 @@ on: [push, pull_request, workflow_dispatch] jobs: - tests-on-legacy-versions: - name: Run tests on legacy versions - runs-on: ubuntu-20.04 - strategy: - matrix: - python-version: [3.7] - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - run: pip install tox - - run: tox - env: - TOXENV: ${{ matrix.python-version }} - test-on-different-versions: name: Run tests runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.9, "3.10", 3.11, 3.12, 3.13] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 diff --git a/pyproject.toml b/pyproject.toml index 794b36d0..ec5daadd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ maintainers = [ description = "Dependency injection framework for Python" readme = {file = "README.rst", content-type = "text/x-rst"} license = {file = "LICENSE.rst", content-type = "text/x-rst"} -requires-python = ">=3.7" +requires-python = ">=3.8" keywords = [ "Dependency injection", "DI", @@ -31,7 +31,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", diff --git a/src/dependency_injector/providers.pyx b/src/dependency_injector/providers.pyx index 27fda027..d276903b 100644 --- a/src/dependency_injector/providers.pyx +++ b/src/dependency_injector/providers.pyx @@ -4,7 +4,6 @@ from __future__ import absolute_import import asyncio import builtins -import contextvars import copy import errno import functools @@ -17,6 +16,7 @@ import sys import threading import warnings from configparser import ConfigParser as IniConfigParser +from contextvars import ContextVar try: from inspect import _is_coroutine_mark as _is_coroutine_marker @@ -3223,15 +3223,10 @@ cdef class ContextLocalSingleton(BaseSingleton): :param provides: Provided type. :type provides: type """ - if not contextvars: - raise RuntimeError( - "Contextvars library not found. This provider " - "requires Python 3.7 or a backport of contextvars. " - "To install a backport run \"pip install contextvars\"." - ) + super(ContextLocalSingleton, self).__init__(provides, *args, **kwargs) - self._storage = contextvars.ContextVar("_storage", default=self._none) + self._storage = ContextVar("_storage", default=self._none) def reset(self): """Reset cached instance, if any. diff --git a/tests/unit/providers/resource/test_async_resource_py35.py b/tests/unit/providers/resource/test_async_resource_py35.py index ba983d60..1ca950a8 100644 --- a/tests/unit/providers/resource/test_async_resource_py35.py +++ b/tests/unit/providers/resource/test_async_resource_py35.py @@ -34,7 +34,6 @@ async def test_init_async_function(): @mark.asyncio -@mark.skipif(sys.version_info < (3, 6), reason="requires Python 3.6+") async def test_init_async_generator(): resource = object() diff --git a/tox.ini b/tox.ini index b37bfeaa..29bc5a4f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] parallel_show_output = true envlist= - coveralls, pylint, flake8, pydocstyle, pydantic-v1, pydantic-v2, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, pypy3.9, pypy3.10 + coveralls, pylint, flake8, pydocstyle, pydantic-v1, pydantic-v2, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, pypy3.9, pypy3.10 [testenv] deps= From dbf86e4eb40065f6542d2c9e01d7e84f6a1ec544 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 18 May 2025 12:17:54 +0300 Subject: [PATCH 05/20] Do not override methods without patching (#886) --- src/dependency_injector/wiring.py | 4 +++ tests/unit/wiring/test_no_interference.py | 40 +++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 tests/unit/wiring/test_no_interference.py diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index a67cf76b..1effd16f 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -523,6 +523,10 @@ def _patch_method( _bind_injections(fn, providers_map) + if fn is method: + # Hotfix, see: https://github.com/ets-labs/python-dependency-injector/issues/884 + return + if isinstance(method, (classmethod, staticmethod)): fn = type(method)(fn) diff --git a/tests/unit/wiring/test_no_interference.py b/tests/unit/wiring/test_no_interference.py new file mode 100644 index 00000000..21f8b0e9 --- /dev/null +++ b/tests/unit/wiring/test_no_interference.py @@ -0,0 +1,40 @@ +from typing import Any, Iterator + +from pytest import fixture + +from dependency_injector.containers import DeclarativeContainer +from dependency_injector.providers import Object +from dependency_injector.wiring import Provide, inject + + +class A: + @inject + def foo(self, value: str = Provide["value"]) -> str: + return "A" + value + + +class B(A): ... + + +class C(A): + def foo(self, *args: Any, **kwargs: Any) -> str: + return "C" + super().foo() + + +class D(B, C): ... + + +class Container(DeclarativeContainer): + value = Object("X") + + +@fixture +def container() -> Iterator[Container]: + c = Container() + c.wire(modules=[__name__]) + yield c + c.unwire() + + +def test_preserve_mro(container: Container) -> None: + assert D().foo() == "CAX" From 8bf9ed04c8c47d35475b12375321ff38c2ed95ca Mon Sep 17 00:00:00 2001 From: ZipFile Date: Sun, 18 May 2025 13:55:06 +0300 Subject: [PATCH 06/20] Update Cython to v3.1 (#887) --- requirements-dev.txt | 2 +- setup.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 0d759d4e..e0def494 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -cython==3.0.11 +cython==3.1.0 setuptools pytest pytest-asyncio diff --git a/setup.py b/setup.py index 429e672c..877d9472 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,9 @@ from Cython.Compiler import Options from setuptools import Extension, setup debug = os.environ.get("DEPENDENCY_INJECTOR_DEBUG_MODE") == "1" +limited_api = os.environ.get("DEPENDENCY_INJECTOR_LIMITED_API") == "1" defined_macros = [] +options = {} compiler_directives = { "language_level": 3, "profile": debug, @@ -17,6 +19,7 @@ Options.annotate = debug # Adding debug options: if debug: + limited_api = False # line tracing is not part of the Limited API defined_macros.extend( [ ("CYTHON_TRACE", "1"), @@ -25,14 +28,20 @@ if debug: ] ) +if limited_api: + options.setdefault("bdist_wheel", {}) + options["bdist_wheel"]["py_limited_api"] = "cp38" + defined_macros.append(("Py_LIMITED_API", 0x03080000)) setup( + options=options, ext_modules=cythonize( [ Extension( "*", ["src/**/*.pyx"], define_macros=defined_macros, + py_limited_api=limited_api, ), ], annotate=debug, From 8814db3fb3b8ec5cded2bcec8a53ac1a2497b0e8 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 16:13:37 -0400 Subject: [PATCH 07/20] Fix annotated attribute injection (#889) * Add example for Annotated attribute injection for module/class attributes * Fix attribute injection with Annotated types * Add unit tests for Annotated attribute and argument injection in wiring * Add .cursor to .gitignore * Style: add blank lines between class definitions and attributes in annotated attribute example * Docs: clarify and format module/class attribute injection for classic and Annotated forms * Changelog: add note and discussion link for Annotated attribute injection support * Fix nls * Fix CI checks and Python 3.8 tests * Fix PR issues * Fix Python 3.8 tests * Fix flake8 issues * Fix: robust Annotated detection for wiring across Python versions * Refactor: extract annotation retrieval and improve typing for Python 3.9 compatibility * Update src/dependency_injector/wiring.py Co-authored-by: ZipFile --------- Co-authored-by: ZipFile --- .gitignore | 3 + docs/main/changelog.rst | 8 + docs/wiring.rst | 32 +++- .../wiring/example_attribute_annotated.py | 31 +++ setup.cfg | 2 +- src/dependency_injector/wiring.py | 30 ++- tests/unit/samples/wiring/module_annotated.py | 126 +++++++++++++ .../provider_ids/test_main_annotated_py36.py | 176 ++++++++++++++++++ tox.ini | 4 +- 9 files changed, 405 insertions(+), 7 deletions(-) create mode 100644 examples/wiring/example_attribute_annotated.py create mode 100644 tests/unit/samples/wiring/module_annotated.py create mode 100644 tests/unit/wiring/provider_ids/test_main_annotated_py36.py diff --git a/.gitignore b/.gitignore index a9f80bee..eef3cb91 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,6 @@ src/**/*.html .workspace/ .vscode/ + +# Cursor project files +.cursor diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 4d724da1..165d003f 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -7,6 +7,14 @@ that were made in every particular version. From version 0.7.6 *Dependency Injector* framework strictly follows `Semantic versioning`_ +Develop +------- + +- Add support for ``Annotated`` type for module and class attribute injection in wiring, + with updated documentation and examples. + See discussion: + https://github.com/ets-labs/python-dependency-injector/pull/721#issuecomment-2025263718 + 4.46.0 ------ diff --git a/docs/wiring.rst b/docs/wiring.rst index 74026879..02f64c60 100644 --- a/docs/wiring.rst +++ b/docs/wiring.rst @@ -254,13 +254,43 @@ To inject a container use special identifier ````: Making injections into modules and class attributes --------------------------------------------------- -You can use wiring to make injections into modules and class attributes. +You can use wiring to make injections into modules and class attributes. Both the classic marker +syntax and the ``Annotated`` form are supported. + +Classic marker syntax: + +.. code-block:: python + + service: Service = Provide[Container.service] + + class Main: + service: Service = Provide[Container.service] + +Full example of the classic marker syntax: .. literalinclude:: ../examples/wiring/example_attribute.py :language: python :lines: 3- :emphasize-lines: 14,19 +Annotated form (Python 3.9+): + +.. code-block:: python + + from typing import Annotated + + service: Annotated[Service, Provide[Container.service]] + + class Main: + service: Annotated[Service, Provide[Container.service]] + +Full example of the annotated form: + +.. literalinclude:: ../examples/wiring/example_attribute_annotated.py + :language: python + :lines: 3- + :emphasize-lines: 16,21 + You could also use string identifiers to avoid a dependency on a container: .. code-block:: python diff --git a/examples/wiring/example_attribute_annotated.py b/examples/wiring/example_attribute_annotated.py new file mode 100644 index 00000000..ae43caa0 --- /dev/null +++ b/examples/wiring/example_attribute_annotated.py @@ -0,0 +1,31 @@ +"""Wiring attribute example with Annotated.""" + +from typing import Annotated + +from dependency_injector import containers, providers +from dependency_injector.wiring import Provide + + +class Service: + ... + + +class Container(containers.DeclarativeContainer): + + service = providers.Factory(Service) + + +service: Annotated[Service, Provide[Container.service]] + + +class Main: + + service: Annotated[Service, Provide[Container.service]] + + +if __name__ == "__main__": + container = Container() + container.wire(modules=[__name__]) + + assert isinstance(service, Service) + assert isinstance(Main.service, Service) diff --git a/setup.cfg b/setup.cfg index 9bb1e56b..5da18a25 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,7 @@ per-file-ignores = examples/containers/traverse.py: E501 examples/providers/async.py: F841 examples/providers/async_overriding.py: F841 - examples/wiring/*: F841 + examples/wiring/*: F821,F841 [pydocstyle] ignore = D100,D101,D102,D105,D106,D107,D203,D213 diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 1effd16f..6829d53b 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -415,7 +415,7 @@ def wire( # noqa: C901 providers_map = ProvidersMap(container) for module in modules: - for member_name, member in inspect.getmembers(module): + for member_name, member in _get_members_and_annotated(module): if _inspect_filter.is_excluded(member): continue @@ -426,7 +426,7 @@ def wire( # noqa: C901 elif inspect.isclass(member): cls = member try: - cls_members = inspect.getmembers(cls) + cls_members = _get_members_and_annotated(cls) except Exception: # noqa # Hotfix, see: https://github.com/ets-labs/python-dependency-injector/issues/441 continue @@ -579,7 +579,11 @@ def _unpatch_attribute(patched: PatchedAttribute) -> None: def _extract_marker(parameter: inspect.Parameter) -> Optional["_Marker"]: if get_origin(parameter.annotation) is Annotated: - marker = get_args(parameter.annotation)[1] + args = get_args(parameter.annotation) + if len(args) > 1: + marker = args[1] + else: + marker = None else: marker = parameter.default @@ -1025,3 +1029,23 @@ def _get_sync_patched(fn: F, patched: PatchedCallable) -> F: patched.closing, ) return cast(F, _patched) + + +if sys.version_info >= (3, 10): + def _get_annotations(obj: Any) -> Dict[str, Any]: + return inspect.get_annotations(obj) +else: + def _get_annotations(obj: Any) -> Dict[str, Any]: + return getattr(obj, "__annotations__", {}) + + +def _get_members_and_annotated(obj: Any) -> Iterable[Tuple[str, Any]]: + members = inspect.getmembers(obj) + annotations = _get_annotations(obj) + for annotation_name, annotation in annotations.items(): + if get_origin(annotation) is Annotated: + args = get_args(annotation) + if len(args) > 1: + member = args[1] + members.append((annotation_name, member)) + return members diff --git a/tests/unit/samples/wiring/module_annotated.py b/tests/unit/samples/wiring/module_annotated.py new file mode 100644 index 00000000..f954d0cb --- /dev/null +++ b/tests/unit/samples/wiring/module_annotated.py @@ -0,0 +1,126 @@ +"""Test module for wiring with Annotated.""" + +import sys +import pytest + +if sys.version_info < (3, 9): + pytest.skip("Annotated is only available in Python 3.9+", allow_module_level=True) + +from decimal import Decimal +from typing import Callable, Annotated + +from dependency_injector import providers +from dependency_injector.wiring import inject, Provide, Provider + +from .container import Container, SubContainer +from .service import Service + +service: Annotated[Service, Provide[Container.service]] +service_provider: Annotated[Callable[..., Service], Provider[Container.service]] +undefined: Annotated[Callable, Provide[providers.Provider()]] + +class TestClass: + service: Annotated[Service, Provide[Container.service]] + service_provider: Annotated[Callable[..., Service], Provider[Container.service]] + undefined: Annotated[Callable, Provide[providers.Provider()]] + + @inject + def __init__(self, service: Annotated[Service, Provide[Container.service]]): + self.service = service + + @inject + def method(self, service: Annotated[Service, Provide[Container.service]]): + return service + + @classmethod + @inject + def class_method(cls, service: Annotated[Service, Provide[Container.service]]): + return service + + @staticmethod + @inject + def static_method(service: Annotated[Service, Provide[Container.service]]): + return service + +@inject +def test_function(service: Annotated[Service, Provide[Container.service]]): + return service + +@inject +def test_function_provider(service_provider: Annotated[Callable[..., Service], Provider[Container.service]]): + service = service_provider() + return service + +@inject +def test_config_value( + value_int: Annotated[int, Provide[Container.config.a.b.c.as_int()]], + value_float: Annotated[float, Provide[Container.config.a.b.c.as_float()]], + value_str: Annotated[str, Provide[Container.config.a.b.c.as_(str)]], + value_decimal: Annotated[Decimal, Provide[Container.config.a.b.c.as_(Decimal)]], + value_required: Annotated[str, Provide[Container.config.a.b.c.required()]], + value_required_int: Annotated[int, Provide[Container.config.a.b.c.required().as_int()]], + value_required_float: Annotated[float, Provide[Container.config.a.b.c.required().as_float()]], + value_required_str: Annotated[str, Provide[Container.config.a.b.c.required().as_(str)]], + value_required_decimal: Annotated[str, Provide[Container.config.a.b.c.required().as_(Decimal)]], +): + return ( + value_int, + value_float, + value_str, + value_decimal, + value_required, + value_required_int, + value_required_float, + value_required_str, + value_required_decimal, + ) + +@inject +def test_config_value_required_undefined( + value_required: Annotated[int, Provide[Container.config.a.b.c.required()]], +): + return value_required + +@inject +def test_provide_provider(service_provider: Annotated[Callable[..., Service], Provide[Container.service.provider]]): + service = service_provider() + return service + +@inject +def test_provider_provider(service_provider: Annotated[Callable[..., Service], Provider[Container.service.provider]]): + service = service_provider() + return service + +@inject +def test_provided_instance(some_value: Annotated[int, Provide[Container.service.provided.foo["bar"].call()]]): + return some_value + +@inject +def test_subcontainer_provider(some_value: Annotated[int, Provide[Container.sub.int_object]]): + return some_value + +@inject +def test_config_invariant(some_value: Annotated[int, Provide[Container.config.option[Container.config.switch]]]): + return some_value + +@inject +def test_provide_from_different_containers( + service: Annotated[Service, Provide[Container.service]], + some_value: Annotated[int, Provide[SubContainer.int_object]], +): + return service, some_value + +class ClassDecorator: + def __init__(self, fn): + self._fn = fn + + def __call__(self, *args, **kwargs): + return self._fn(*args, **kwargs) + +@ClassDecorator +@inject +def test_class_decorator(service: Annotated[Service, Provide[Container.service]]): + return service + +def test_container(container: Annotated[Container, Provide[Container]]): + return container.service() diff --git a/tests/unit/wiring/provider_ids/test_main_annotated_py36.py b/tests/unit/wiring/provider_ids/test_main_annotated_py36.py new file mode 100644 index 00000000..34d1d747 --- /dev/null +++ b/tests/unit/wiring/provider_ids/test_main_annotated_py36.py @@ -0,0 +1,176 @@ +"""Main wiring tests for Annotated attribute and argument injection.""" + +from decimal import Decimal +import typing + +from dependency_injector import errors +from dependency_injector.wiring import Closing, Provide, Provider, wire +from pytest import fixture, mark, raises + +from samples.wiring import module_annotated as module, package, resourceclosing +from samples.wiring.service import Service +from samples.wiring.container import Container, SubContainer + +@fixture(autouse=True) +def container(): + container = Container(config={"a": {"b": {"c": 10}}}) + container.wire( + modules=[module], + packages=[package], + ) + yield container + container.unwire() + +@fixture +def subcontainer(): + container = SubContainer() + container.wire( + modules=[module], + packages=[package], + ) + yield container + container.unwire() + +@fixture +def resourceclosing_container(): + container = resourceclosing.Container() + container.wire(modules=[resourceclosing]) + yield container + container.unwire() + +def test_module_attributes_wiring(): + assert isinstance(module.service, Service) + assert isinstance(module.service_provider(), Service) + assert isinstance(module.__annotations__['undefined'], typing._AnnotatedAlias) + +def test_class_wiring(): + test_class_object = module.TestClass() + assert isinstance(test_class_object.service, Service) + +def test_class_wiring_context_arg(container: Container): + test_service = container.service() + test_class_object = module.TestClass(service=test_service) + assert test_class_object.service is test_service + +def test_class_method_wiring(): + test_class_object = module.TestClass() + service = test_class_object.method() + assert isinstance(service, Service) + +def test_class_classmethod_wiring(): + service = module.TestClass.class_method() + assert isinstance(service, Service) + +def test_instance_classmethod_wiring(): + instance = module.TestClass() + service = instance.class_method() + assert isinstance(service, Service) + +def test_class_staticmethod_wiring(): + service = module.TestClass.static_method() + assert isinstance(service, Service) + +def test_instance_staticmethod_wiring(): + instance = module.TestClass() + service = instance.static_method() + assert isinstance(service, Service) + +def test_class_attribute_wiring(): + assert isinstance(module.TestClass.service, Service) + assert isinstance(module.TestClass.service_provider(), Service) + assert isinstance(module.TestClass.__annotations__['undefined'], typing._AnnotatedAlias) + +def test_function_wiring(): + service = module.test_function() + assert isinstance(service, Service) + +def test_function_wiring_context_arg(container: Container): + test_service = container.service() + service = module.test_function(service=test_service) + assert service is test_service + +def test_function_wiring_provider(): + service = module.test_function_provider() + assert isinstance(service, Service) + +def test_function_wiring_provider_context_arg(container: Container): + test_service = container.service() + service = module.test_function_provider(service_provider=lambda: test_service) + assert service is test_service + +def test_configuration_option(): + ( + value_int, + value_float, + value_str, + value_decimal, + value_required, + value_required_int, + value_required_float, + value_required_str, + value_required_decimal, + ) = module.test_config_value() + + assert value_int == 10 + assert value_float == 10.0 + assert value_str == "10" + assert value_decimal == Decimal(10) + assert value_required == 10 + assert value_required_int == 10 + assert value_required_float == 10.0 + assert value_required_str == "10" + assert value_required_decimal == Decimal(10) + +def test_configuration_option_required_undefined(container: Container): + container.config.reset_override() + with raises(errors.Error, match="Undefined configuration option \"config.a.b.c\""): + module.test_config_value_required_undefined() + +def test_provide_provider(): + service = module.test_provide_provider() + assert isinstance(service, Service) + +def test_provider_provider(): + service = module.test_provider_provider() + assert isinstance(service, Service) + +def test_provided_instance(container: Container): + class TestService: + foo = {"bar": lambda: 10} + + with container.service.override(TestService()): + some_value = module.test_provided_instance() + assert some_value == 10 + +def test_subcontainer(): + some_value = module.test_subcontainer_provider() + assert some_value == 1 + +def test_config_invariant(container: Container): + config = { + "option": { + "a": 1, + "b": 2, + }, + "switch": "a", + } + container.config.from_dict(config) + + value_default = module.test_config_invariant() + assert value_default == 1 + + with container.config.switch.override("a"): + value_a = module.test_config_invariant() + assert value_a == 1 + + with container.config.switch.override("b"): + value_b = module.test_config_invariant() + assert value_b == 2 + +def test_class_decorator(): + service = module.test_class_decorator() + assert isinstance(service, Service) + +def test_container(): + service = module.test_container() + assert isinstance(service, Service) diff --git a/tox.ini b/tox.ini index 29bc5a4f..b524b88d 100644 --- a/tox.ini +++ b/tox.ini @@ -89,8 +89,8 @@ commands= deps= flake8 commands= - flake8 --max-complexity=10 src/dependency_injector/ - flake8 --max-complexity=10 examples/ + flake8 src/dependency_injector/ + flake8 examples/ [testenv:pydocstyle] deps= From f50cc95405bf1b8c3908aa0e4dcfdce1b8af8969 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 21 May 2025 16:17:41 -0400 Subject: [PATCH 08/20] Add Cursor rules --- .cursor/rules/coding-guide.mdc | 29 +++++++++++++++++++++++++++++ .cursor/rules/makefile-commands.mdc | 7 +++++++ .cursor/rules/run-examples.mdc | 8 ++++++++ .gitignore | 3 --- 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 .cursor/rules/coding-guide.mdc create mode 100644 .cursor/rules/makefile-commands.mdc create mode 100644 .cursor/rules/run-examples.mdc diff --git a/.cursor/rules/coding-guide.mdc b/.cursor/rules/coding-guide.mdc new file mode 100644 index 00000000..6251f889 --- /dev/null +++ b/.cursor/rules/coding-guide.mdc @@ -0,0 +1,29 @@ +--- +description: Code in Python and Cython +globs: +alwaysApply: false +--- +- Follow PEP 8 rules +- When you write imports, split system, 3rd-party, and local imports with a new line +- Have two empty lines between the import block and the rest of the code +- Have an empty line (\n) at the end of every file +- If a file is supposed to be run, always add ``if __name__ == 'main'`` +- Always follow a consistent pattern of using double or single quotes +- When there is a class without a docblock, leave one blank line before its members, e.g.: +```python +class Container(containers.DeclarativeContainer): + + service = providers.Factory(Service) +``` + +- Avoid shortcuts in names unless absolutely necessarry, exceptions: +``` +arg +args +kwarg +kwargs +obj +cls +``` + +- Avoid inline comments unless absolutely necessarry diff --git a/.cursor/rules/makefile-commands.mdc b/.cursor/rules/makefile-commands.mdc new file mode 100644 index 00000000..b43fb75b --- /dev/null +++ b/.cursor/rules/makefile-commands.mdc @@ -0,0 +1,7 @@ +--- +description: Build and run tests +globs: +alwaysApply: false +--- +- Use Makefile commands to build, test, lint and other similar operations when they are available. +- Activate virtualenv before running any commands by ``. venv/bin/actvate`` diff --git a/.cursor/rules/run-examples.mdc b/.cursor/rules/run-examples.mdc new file mode 100644 index 00000000..f5f3b703 --- /dev/null +++ b/.cursor/rules/run-examples.mdc @@ -0,0 +1,8 @@ +--- +description: Run examples +globs: +alwaysApply: false +--- +- When you run an example from the ``examples/`` folder, switch to the example folder and run it from there. +- If there are instructions on running the examples or its tests in readme, follow them +- Activate virtualenv before running any commands by ``. venv/bin/actvate`` diff --git a/.gitignore b/.gitignore index eef3cb91..a9f80bee 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,3 @@ src/**/*.html .workspace/ .vscode/ - -# Cursor project files -.cursor From e7e64f6ae02931b14e0a0ce654da99077a703261 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Thu, 22 May 2025 18:29:37 -0400 Subject: [PATCH 09/20] Update coding-guide.mdc --- .cursor/rules/coding-guide.mdc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.cursor/rules/coding-guide.mdc b/.cursor/rules/coding-guide.mdc index 6251f889..60f28e5d 100644 --- a/.cursor/rules/coding-guide.mdc +++ b/.cursor/rules/coding-guide.mdc @@ -16,7 +16,7 @@ class Container(containers.DeclarativeContainer): service = providers.Factory(Service) ``` -- Avoid shortcuts in names unless absolutely necessarry, exceptions: +- Avoid shortcuts in names unless absolutely necessary, exceptions: ``` arg args @@ -26,4 +26,4 @@ obj cls ``` -- Avoid inline comments unless absolutely necessarry +- Avoid inline comments unless absolutely necessary From 383e95faed0f0dfc952e2cc93ea7eed2c7333695 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 19 May 2025 13:08:15 +0000 Subject: [PATCH 10/20] Fix file inclusion warnings from MANIFEST.in --- MANIFEST.in | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index b29eeb36..7205ddc5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,7 @@ -recursive-include src/dependency_injector *.py* *.c +recursive-include src/dependency_injector *.py* *.c py.typed recursive-include tests *.py include README.rst include CONTRIBUTORS.rst include LICENSE.rst -include requirements.txt include setup.py include tox.ini -include py.typed From 49cc8ed827583f3b33b283d055bf7de44cde6707 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 19 May 2025 13:27:05 +0000 Subject: [PATCH 11/20] Fix PyPy test runs in tox --- tox.ini | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/tox.ini b/tox.ini index b524b88d..705ea890 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] parallel_show_output = true envlist= - coveralls, pylint, flake8, pydocstyle, pydantic-v1, pydantic-v2, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, pypy3.9, pypy3.10 + coveralls, pylint, flake8, pydocstyle, pydantic-v1, pydantic-v2, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, pypy3.9, pypy3.10, pypy3.11 [testenv] deps= @@ -20,7 +20,6 @@ deps= extras= yaml commands = pytest -python_files = test_*_py3*.py setenv = COVERAGE_RCFILE = pyproject.toml @@ -61,22 +60,6 @@ commands= coverage report coveralls -[testenv:pypy3.9] -deps= - pytest - pytest-asyncio - httpx - flask - pydantic-settings - werkzeug - fastapi - boto3 - mypy_boto3_s3 -extras= - yaml -commands = pytest - - [testenv:pylint] deps= pylint From 561ff4665800f5eeb63bf919c7c96e9c8ba42276 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 19 May 2025 13:29:40 +0000 Subject: [PATCH 12/20] Add wheelhouse to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a9f80bee..86ecf7c7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ lib64/ parts/ sdist/ var/ +wheelhouse/ *.egg-info/ .installed.cfg *.egg From 183b2ae7ff9cb33c3dbe9f2e82040b01ee6ddecf Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 19 May 2025 13:31:34 +0000 Subject: [PATCH 13/20] Require Cython>=3.1.1 --- .github/workflows/tests-and-linters.yml | 2 +- pyproject.toml | 2 +- requirements-dev.txt | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests-and-linters.yml b/.github/workflows/tests-and-linters.yml index f43a2db8..4ee0d192 100644 --- a/.github/workflows/tests-and-linters.yml +++ b/.github/workflows/tests-and-linters.yml @@ -44,7 +44,7 @@ jobs: - uses: actions/setup-python@v4 with: python-version: 3.12 - - run: pip install tox 'cython>=3,<4' + - run: pip install tox - run: tox -vv env: TOXENV: coveralls diff --git a/pyproject.toml b/pyproject.toml index ec5daadd..80a054e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools", "Cython"] +requires = ["setuptools", "Cython>=3.1.1"] build-backend = "setuptools.build_meta" [project] diff --git a/requirements-dev.txt b/requirements-dev.txt index e0def494..47e3ca42 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -cython==3.1.0 +cython==3.1.1 setuptools pytest pytest-asyncio @@ -13,7 +13,8 @@ mypy pyyaml httpx fastapi -pydantic==1.10.17 +pydantic +pydantic-settings numpy scipy boto3 From cfeb018ca75b05b3116f0be63761f9dea1440099 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 19 May 2025 13:32:00 +0000 Subject: [PATCH 14/20] Fix pytest-asyncio warning --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 80a054e7..7512cb94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,6 +102,7 @@ max-public-methods = 30 [tool.pytest.ini_options] testpaths = ["tests/unit/"] asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" markers = [ "pydantic: Tests with Pydantic as a dependency", ] From 14be69371b3155f2e924931b21e8bcd38bda8f33 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 19 May 2025 13:34:29 +0000 Subject: [PATCH 15/20] Limit ABI3 builds to CPython only --- setup.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 877d9472..5f4669e4 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,17 @@ """`Dependency injector` setup script.""" import os +import sys from Cython.Build import cythonize from Cython.Compiler import Options from setuptools import Extension, setup debug = os.environ.get("DEPENDENCY_INJECTOR_DEBUG_MODE") == "1" -limited_api = os.environ.get("DEPENDENCY_INJECTOR_LIMITED_API") == "1" +limited_api = ( + os.environ.get("DEPENDENCY_INJECTOR_LIMITED_API") == "1" + and sys.implementation.name == "cpython" +) defined_macros = [] options = {} compiler_directives = { @@ -31,7 +35,7 @@ if debug: if limited_api: options.setdefault("bdist_wheel", {}) options["bdist_wheel"]["py_limited_api"] = "cp38" - defined_macros.append(("Py_LIMITED_API", 0x03080000)) + defined_macros.append(("Py_LIMITED_API", "0x03080000")) setup( options=options, From a61749c68dfa8061750350daa79dcb8ea0ecafd9 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Mon, 19 May 2025 13:35:23 +0000 Subject: [PATCH 16/20] Enable ABI3 builds --- .github/workflows/publishing.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publishing.yml b/.github/workflows/publishing.yml index f3bf1529..27bed3b7 100644 --- a/.github/workflows/publishing.yml +++ b/.github/workflows/publishing.yml @@ -62,11 +62,14 @@ jobs: matrix: os: [ubuntu-24.04, ubuntu-24.04-arm, windows-2019, macos-14] env: - CIBW_SKIP: cp27-* + CIBW_ENABLE: pypy + CIBW_ENVIRONMENT: >- + PIP_CONFIG_SETTINGS="build_ext=-j4" + DEPENDENCY_INJECTOR_LIMITED_API="1" steps: - uses: actions/checkout@v3 - name: Build wheels - uses: pypa/cibuildwheel@v2.20.0 + uses: pypa/cibuildwheel@v2.23.3 - uses: actions/upload-artifact@v4 with: name: cibw-wheels-x86-${{ matrix.os }}-${{ strategy.job-index }} From dfee54932b4a067806230417c869855e49be6edf Mon Sep 17 00:00:00 2001 From: ZipFile Date: Thu, 22 May 2025 20:02:39 +0000 Subject: [PATCH 17/20] Migrate to OIDC publishing --- .github/workflows/publishing.yml | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publishing.yml b/.github/workflows/publishing.yml index 27bed3b7..893536a4 100644 --- a/.github/workflows/publishing.yml +++ b/.github/workflows/publishing.yml @@ -75,10 +75,13 @@ jobs: name: cibw-wheels-x86-${{ matrix.os }}-${{ strategy.job-index }} path: ./wheelhouse/*.whl - publish: - name: Publish on PyPI + test-publish: + name: Upload release to TestPyPI needs: [build-sdist, build-wheels] - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest + environment: test-pypi + permissions: + id-token: write steps: - uses: actions/download-artifact@v4 with: @@ -87,11 +90,22 @@ jobs: merge-multiple: true - uses: pypa/gh-action-pypi-publish@release/v1 with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} -# For publishing to Test PyPI, uncomment next two lines: -# password: ${{ secrets.TEST_PYPI_API_TOKEN }} -# repository_url: https://test.pypi.org/legacy/ + repository-url: https://test.pypi.org/legacy/ + + publish: + name: Upload release to PyPI + needs: [build-sdist, build-wheels, test-publish] + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + pattern: cibw-* + path: dist + merge-multiple: true + - uses: pypa/gh-action-pypi-publish@release/v1 publish-docs: name: Publish docs From 8be79126ad91d8044a77bc04649f5011400059cb Mon Sep 17 00:00:00 2001 From: ZipFile Date: Wed, 28 May 2025 16:51:39 +0000 Subject: [PATCH 18/20] Enable ABI3 for regular tests --- .github/workflows/tests-and-linters.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests-and-linters.yml b/.github/workflows/tests-and-linters.yml index 4ee0d192..bf07a2d9 100644 --- a/.github/workflows/tests-and-linters.yml +++ b/.github/workflows/tests-and-linters.yml @@ -18,6 +18,7 @@ jobs: - run: pip install tox - run: tox env: + DEPENDENCY_INJECTOR_LIMITED_API: 1 TOXENV: ${{ matrix.python-version }} test-different-pydantic-versions: From 41ed07a210e956a38664cf7c31ed408fa1d2bb2a Mon Sep 17 00:00:00 2001 From: ZipFile Date: Wed, 28 May 2025 19:03:52 +0000 Subject: [PATCH 19/20] Update changelog --- docs/main/changelog.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 165d003f..06acc716 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -7,13 +7,17 @@ that were made in every particular version. From version 0.7.6 *Dependency Injector* framework strictly follows `Semantic versioning`_ -Develop +4.47.0 ------- - Add support for ``Annotated`` type for module and class attribute injection in wiring, with updated documentation and examples. See discussion: https://github.com/ets-labs/python-dependency-injector/pull/721#issuecomment-2025263718 +- Fix ``root`` property shadowing in ``ConfigurationOption`` (`#875 https://github.com/ets-labs/python-dependency-injector/pull/875`_) +- Fix incorrect monkeypatching during ``wire()`` that could violate MRO in some classes (`#886 https://github.com/ets-labs/python-dependency-injector/pull/886`_) +- ABI3 wheels are now published for CPython. +- Drop support of Python 3.7. 4.46.0 ------ From 01349c43e1e24b0778d1210399ed1c7bd5250dd8 Mon Sep 17 00:00:00 2001 From: ZipFile Date: Wed, 28 May 2025 19:05:05 +0000 Subject: [PATCH 20/20] Bump version --- src/dependency_injector/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dependency_injector/__init__.py b/src/dependency_injector/__init__.py index faa905f5..c97f7e90 100644 --- a/src/dependency_injector/__init__.py +++ b/src/dependency_injector/__init__.py @@ -1,6 +1,6 @@ """Top-level package.""" -__version__ = "4.46.0" +__version__ = "4.47.0" """Version number. :type: str