Merge branch 'release/4.47.0'

This commit is contained in:
ZipFile 2025-05-28 19:09:56 +00:00
commit 193249f7ec
26 changed files with 578 additions and 114 deletions

View File

@ -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 necessary, exceptions:
```
arg
args
kwarg
kwargs
obj
cls
```
- Avoid inline comments unless absolutely necessary

View File

@ -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``

View File

@ -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``

View File

@ -62,20 +62,26 @@ 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 }}
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:
@ -84,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

View File

@ -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
@ -34,6 +18,7 @@ jobs:
- run: pip install tox
- run: tox
env:
DEPENDENCY_INJECTOR_LIMITED_API: 1
TOXENV: ${{ matrix.python-version }}
test-different-pydantic-versions:
@ -60,7 +45,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

1
.gitignore vendored
View File

@ -15,6 +15,7 @@ lib64/
parts/
sdist/
var/
wheelhouse/
*.egg-info/
.installed.cfg
*.egg

View File

@ -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

View File

@ -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

View File

@ -7,6 +7,18 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly
follows `Semantic versioning`_
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
------

View File

@ -254,13 +254,43 @@ To inject a container use special identifier ``<container>``:
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

View File

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

View File

@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools", "Cython"]
requires = ["setuptools", "Cython>=3.1.1"]
build-backend = "setuptools.build_meta"
[project]
@ -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",
@ -99,3 +98,18 @@ ignore = ["tests"]
[tool.pylint.design]
min-public-methods = 0
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",
]
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",
]

View File

@ -1,4 +1,4 @@
cython==3.0.11
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

View File

@ -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

View File

@ -1,13 +1,19 @@
"""`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"
and sys.implementation.name == "cpython"
)
defined_macros = []
options = {}
compiler_directives = {
"language_level": 3,
"profile": debug,
@ -17,6 +23,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 +32,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,

View File

@ -1,6 +1,6 @@
"""Top-level package."""
__version__ = "4.46.0"
__version__ = "4.47.0"
"""Version number.
:type: str

View File

@ -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
@ -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):
@ -3224,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.

View File

@ -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
@ -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
@ -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)
@ -575,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
@ -628,21 +636,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 +657,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:
@ -1037,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

View File

@ -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.*

View File

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

View File

@ -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()

View File

@ -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()

View File

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

View File

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

View File

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

29
tox.ini
View File

@ -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, pypy3.11
[testenv]
deps=
@ -19,8 +19,7 @@ deps=
werkzeug
extras=
yaml
commands = pytest -c tests/.configs/pytest.ini
python_files = test_*_py3*.py
commands = pytest
setenv =
COVERAGE_RCFILE = pyproject.toml
@ -45,7 +44,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,26 +56,10 @@ deps=
coveralls>=4
commands=
coverage erase
coverage run -m pytest -c tests/.configs/pytest.ini
coverage run -m pytest
coverage report
coveralls
[testenv:pypy3.9]
deps=
pytest
pytest-asyncio
httpx
flask
pydantic-settings
werkzeug
fastapi
boto3
mypy_boto3_s3
extras=
yaml
commands = pytest -c tests/.configs/pytest-py35.ini
[testenv:pylint]
deps=
pylint
@ -89,8 +72,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=