Merge branch 'release/4.48.2'

This commit is contained in:
ZipFile 2025-09-19 10:03:34 +00:00
commit 5a1aef9203
16 changed files with 276 additions and 28 deletions

View File

@ -1,6 +1,10 @@
VERSION := $(shell python setup.py --version) VERSION := $(shell python setup.py --version)
export COVERAGE_RCFILE := pyproject.toml export COVERAGE_RCFILE := pyproject.toml
export CIBW_ENVIRONMENT_PASS_LINUX := CFLAGS PIP_CONFIG_SETTINGS DEPENDENCY_INJECTOR_LIMITED_API
export PIP_CONFIG_SETTINGS ?= build_ext=-j4
export DEPENDENCY_INJECTOR_LIMITED_API ?= 1
export CFLAGS ?= -g0
clean: clean:
# Clean sources # Clean sources
@ -63,3 +67,6 @@ publish:
# Create and upload tag # Create and upload tag
git tag -a $(VERSION) -m 'version $(VERSION)' git tag -a $(VERSION) -m 'version $(VERSION)'
git push --tags git push --tags
wheels:
cibuildwheel --output-dir wheelhouse

View File

@ -7,6 +7,13 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly From version 0.7.6 *Dependency Injector* framework strictly
follows `Semantic versioning`_ follows `Semantic versioning`_
4.48.2
------
- Add ``warn_unresolved=True`` to ``WiringConfiguration`` and ``container.wire()``
to produce warnings on unresolved string identifiers.
- ABI3 wheels are now built only for CPython version >=3.10 (see issue `#919 <https://github.com/ets-labs/python-dependency-injector/issues/919>`_).
4.48.1 4.48.1
------ ------

View File

@ -33,7 +33,7 @@ factories:
- :ref:`factory-specialize-provided-type` - :ref:`factory-specialize-provided-type`
- :ref:`abstract-factory` - :ref:`abstract-factory`
``Singleton`` provider scope is tied to the container. Two different containers will provider ``Singleton`` provider scope is tied to the container. Two different containers will provide
two different singleton objects: two different singleton objects:
.. literalinclude:: ../../examples/providers/singleton_multiple_containers.py .. literalinclude:: ../../examples/providers/singleton_multiple_containers.py

View File

@ -251,6 +251,32 @@ To inject a container use special identifier ``<container>``:
def foo(container: Container = Provide["<container>"]) -> None: def foo(container: Container = Provide["<container>"]) -> None:
... ...
Caveats
~~~~~~~
While using string identifiers you may not notice a typo in the identifier until the code is executed.
In order to aid with catching such errors early, you may pass `warn_unresolved=True` to the ``wire`` method and/or :class:`WiringConfiguration`:
.. code-block:: python
:emphasize-lines: 4
class Container(containers.DeclarativeContainer):
wiring_config = containers.WiringConfiguration(
modules=["yourapp.module"],
warn_unresolved=True,
)
Or:
.. code-block:: python
:emphasize-lines: 4
container = Container()
container.wire(
modules=["yourapp.module"],
warn_unresolved=True,
)
Making injections into modules and class attributes Making injections into modules and class attributes
--------------------------------------------------- ---------------------------------------------------

View File

@ -1,5 +1,5 @@
[build-system] [build-system]
requires = ["setuptools", "Cython>=3.1.1"] requires = ["setuptools", "Cython>=3.1.4"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
@ -54,7 +54,7 @@ classifiers = [
dynamic = ["version"] dynamic = ["version"]
dependencies = [ dependencies = [
# typing.Annotated since v3.9 # typing.Annotated since v3.9
# typing.Self since v3.11 # typing.Self and typing.assert_never since v3.11
"typing-extensions; python_version<'3.11'", "typing-extensions; python_version<'3.11'",
] ]

View File

@ -1,4 +1,4 @@
cython==3.1.1 cython==3.1.4
setuptools setuptools
pytest pytest
pytest-asyncio pytest-asyncio

View File

@ -2,6 +2,7 @@
import os import os
import sys import sys
import sysconfig
from Cython.Build import cythonize from Cython.Build import cythonize
from Cython.Compiler import Options from Cython.Compiler import Options
@ -11,6 +12,8 @@ debug = os.environ.get("DEPENDENCY_INJECTOR_DEBUG_MODE") == "1"
limited_api = ( limited_api = (
os.environ.get("DEPENDENCY_INJECTOR_LIMITED_API") == "1" os.environ.get("DEPENDENCY_INJECTOR_LIMITED_API") == "1"
and sys.implementation.name == "cpython" and sys.implementation.name == "cpython"
and sys.version_info >= (3, 10)
and not sysconfig.get_config_var("Py_GIL_DISABLED")
) )
defined_macros = [] defined_macros = []
options = {} options = {}
@ -34,8 +37,8 @@ if debug:
if limited_api: if limited_api:
options.setdefault("bdist_wheel", {}) options.setdefault("bdist_wheel", {})
options["bdist_wheel"]["py_limited_api"] = "cp38" options["bdist_wheel"]["py_limited_api"] = "cp310"
defined_macros.append(("Py_LIMITED_API", "0x03080000")) defined_macros.append(("Py_LIMITED_API", "0x030A0000"))
setup( setup(
options=options, options=options,

View File

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

View File

@ -72,6 +72,7 @@ class Container:
modules: Optional[Iterable[Any]] = None, modules: Optional[Iterable[Any]] = None,
packages: Optional[Iterable[Any]] = None, packages: Optional[Iterable[Any]] = None,
from_package: Optional[str] = None, from_package: Optional[str] = None,
warn_unresolved: bool = False,
) -> None: ... ) -> None: ...
def unwire(self) -> None: ... def unwire(self) -> None: ...
def init_resources(self, resource_type: Type[Resource[Any]] = Resource) -> Optional[Awaitable[None]]: ... def init_resources(self, resource_type: Type[Resource[Any]] = Resource) -> Optional[Awaitable[None]]: ...

View File

@ -20,15 +20,31 @@ from .wiring import wire, unwire
class WiringConfiguration: class WiringConfiguration:
"""Container wiring configuration.""" """Container wiring configuration."""
def __init__(self, modules=None, packages=None, from_package=None, auto_wire=True, keep_cache=False): def __init__(
self,
modules=None,
packages=None,
from_package=None,
auto_wire=True,
keep_cache=False,
warn_unresolved=False,
):
self.modules = [*modules] if modules else [] self.modules = [*modules] if modules else []
self.packages = [*packages] if packages else [] self.packages = [*packages] if packages else []
self.from_package = from_package self.from_package = from_package
self.auto_wire = auto_wire self.auto_wire = auto_wire
self.keep_cache = keep_cache self.keep_cache = keep_cache
self.warn_unresolved = warn_unresolved
def __deepcopy__(self, memo=None): def __deepcopy__(self, memo=None):
return self.__class__(self.modules, self.packages, self.from_package, self.auto_wire, self.keep_cache) return self.__class__(
self.modules,
self.packages,
self.from_package,
self.auto_wire,
self.keep_cache,
self.warn_unresolved,
)
class Container: class Container:
@ -259,7 +275,14 @@ class DynamicContainer(Container):
"""Check if auto wiring is needed.""" """Check if auto wiring is needed."""
return self.wiring_config.auto_wire is True return self.wiring_config.auto_wire is True
def wire(self, modules=None, packages=None, from_package=None, keep_cache=None): def wire(
self,
modules=None,
packages=None,
from_package=None,
keep_cache=None,
warn_unresolved=False,
):
"""Wire container providers with provided packages and modules. """Wire container providers with provided packages and modules.
:rtype: None :rtype: None
@ -298,6 +321,7 @@ class DynamicContainer(Container):
modules=modules, modules=modules,
packages=packages, packages=packages,
keep_cache=keep_cache, keep_cache=keep_cache,
warn_unresolved=warn_unresolved,
) )
if modules: if modules:

View File

@ -1599,7 +1599,7 @@ cdef class ConfigurationOption(Provider):
return self._root return self._root
def get_name(self): def get_name(self):
return ".".join((self._root.get_name(), self._get_self_name())) return f"{self._root.get_name()}.{self._get_self_name()}"
def get_name_segments(self): def get_name_segments(self):
return self._name return self._name

View File

@ -30,9 +30,9 @@ from typing import (
from warnings import warn from warnings import warn
try: try:
from typing import Self from typing import Self, assert_never
except ImportError: except ImportError:
from typing_extensions import Self from typing_extensions import Self, assert_never
try: try:
from functools import cache from functools import cache
@ -139,6 +139,10 @@ class DIWiringWarning(RuntimeWarning):
"""Base class for all warnings raised by the wiring module.""" """Base class for all warnings raised by the wiring module."""
class UnresolvedMarkerWarning(DIWiringWarning):
"""Warning raised when a marker with string identifier cannot be resolved against container."""
class PatchedRegistry: class PatchedRegistry:
def __init__(self) -> None: def __init__(self) -> None:
@ -433,6 +437,7 @@ def wire( # noqa: C901
modules: Optional[Iterable[ModuleType]] = None, modules: Optional[Iterable[ModuleType]] = None,
packages: Optional[Iterable[ModuleType]] = None, packages: Optional[Iterable[ModuleType]] = None,
keep_cache: bool = False, keep_cache: bool = False,
warn_unresolved: bool = False,
) -> None: ) -> None:
"""Wire container providers with provided packages and modules.""" """Wire container providers with provided packages and modules."""
modules = [*modules] if modules else [] modules = [*modules] if modules else []
@ -449,9 +454,23 @@ def wire( # noqa: C901
continue continue
if _is_marker(member): if _is_marker(member):
_patch_attribute(module, member_name, member, providers_map) _patch_attribute(
module,
member_name,
member,
providers_map,
warn_unresolved=warn_unresolved,
warn_unresolved_stacklevel=1,
)
elif inspect.isfunction(member): elif inspect.isfunction(member):
_patch_fn(module, member_name, member, providers_map) _patch_fn(
module,
member_name,
member,
providers_map,
warn_unresolved=warn_unresolved,
warn_unresolved_stacklevel=1,
)
elif inspect.isclass(member): elif inspect.isclass(member):
cls = member cls = member
try: try:
@ -463,15 +482,30 @@ def wire( # noqa: C901
for cls_member_name, cls_member in cls_members: for cls_member_name, cls_member in cls_members:
if _is_marker(cls_member): if _is_marker(cls_member):
_patch_attribute( _patch_attribute(
cls, cls_member_name, cls_member, providers_map cls,
cls_member_name,
cls_member,
providers_map,
warn_unresolved=warn_unresolved,
warn_unresolved_stacklevel=1,
) )
elif _is_method(cls_member): elif _is_method(cls_member):
_patch_method( _patch_method(
cls, cls_member_name, cls_member, providers_map cls,
cls_member_name,
cls_member,
providers_map,
warn_unresolved=warn_unresolved,
warn_unresolved_stacklevel=1,
) )
for patched in _patched_registry.get_callables_from_module(module): for patched in _patched_registry.get_callables_from_module(module):
_bind_injections(patched, providers_map) _bind_injections(
patched,
providers_map,
warn_unresolved=warn_unresolved,
warn_unresolved_stacklevel=1,
)
if not keep_cache: if not keep_cache:
clear_cache() clear_cache()
@ -524,6 +558,8 @@ def _patch_fn(
name: str, name: str,
fn: Callable[..., Any], fn: Callable[..., Any],
providers_map: ProvidersMap, providers_map: ProvidersMap,
warn_unresolved: bool = False,
warn_unresolved_stacklevel: int = 0,
) -> None: ) -> None:
if not _is_patched(fn): if not _is_patched(fn):
reference_injections, reference_closing = _fetch_reference_injections(fn) reference_injections, reference_closing = _fetch_reference_injections(fn)
@ -531,7 +567,12 @@ def _patch_fn(
return return
fn = _get_patched(fn, reference_injections, reference_closing) fn = _get_patched(fn, reference_injections, reference_closing)
_bind_injections(fn, providers_map) _bind_injections(
fn,
providers_map,
warn_unresolved=warn_unresolved,
warn_unresolved_stacklevel=warn_unresolved_stacklevel + 1,
)
setattr(module, name, fn) setattr(module, name, fn)
@ -541,6 +582,8 @@ def _patch_method(
name: str, name: str,
method: Callable[..., Any], method: Callable[..., Any],
providers_map: ProvidersMap, providers_map: ProvidersMap,
warn_unresolved: bool = False,
warn_unresolved_stacklevel: int = 0,
) -> None: ) -> None:
if ( if (
hasattr(cls, "__dict__") hasattr(cls, "__dict__")
@ -558,7 +601,12 @@ def _patch_method(
return return
fn = _get_patched(fn, reference_injections, reference_closing) fn = _get_patched(fn, reference_injections, reference_closing)
_bind_injections(fn, providers_map) _bind_injections(
fn,
providers_map,
warn_unresolved=warn_unresolved,
warn_unresolved_stacklevel=warn_unresolved_stacklevel + 1,
)
if fn is method: if fn is method:
# Hotfix, see: https://github.com/ets-labs/python-dependency-injector/issues/884 # Hotfix, see: https://github.com/ets-labs/python-dependency-injector/issues/884
@ -594,9 +642,17 @@ def _patch_attribute(
name: str, name: str,
marker: "_Marker", marker: "_Marker",
providers_map: ProvidersMap, providers_map: ProvidersMap,
warn_unresolved: bool = False,
warn_unresolved_stacklevel: int = 0,
) -> None: ) -> None:
provider = providers_map.resolve_provider(marker.provider, marker.modifier) provider = providers_map.resolve_provider(marker.provider, marker.modifier)
if provider is None: if provider is None:
if warn_unresolved:
warn(
f"Unresolved marker {name} in {member!r}",
UnresolvedMarkerWarning,
stacklevel=warn_unresolved_stacklevel + 2,
)
return return
_patched_registry.register_attribute(PatchedAttribute(member, name, marker)) _patched_registry.register_attribute(PatchedAttribute(member, name, marker))
@ -673,7 +729,12 @@ def _fetch_reference_injections( # noqa: C901
return injections, closing return injections, closing
def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> None: def _bind_injections(
fn: Callable[..., Any],
providers_map: ProvidersMap,
warn_unresolved: bool = False,
warn_unresolved_stacklevel: int = 0,
) -> None:
patched_callable = _patched_registry.get_callable(fn) patched_callable = _patched_registry.get_callable(fn)
if patched_callable is None: if patched_callable is None:
return return
@ -682,6 +743,12 @@ def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> Non
provider = providers_map.resolve_provider(marker.provider, marker.modifier) provider = providers_map.resolve_provider(marker.provider, marker.modifier)
if provider is None: if provider is None:
if warn_unresolved:
warn(
f"Unresolved marker {injection} in {fn.__qualname__}",
UnresolvedMarkerWarning,
stacklevel=warn_unresolved_stacklevel + 2,
)
continue continue
if isinstance(marker, Provide): if isinstance(marker, Provide):
@ -791,6 +858,9 @@ class TypeModifier(Modifier):
) -> providers.Provider: ) -> providers.Provider:
return provider.as_(self.type_) return provider.as_(self.type_)
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.type_!r})"
def as_int() -> TypeModifier: def as_int() -> TypeModifier:
"""Return int type modifier.""" """Return int type modifier."""
@ -809,8 +879,8 @@ def as_(type_: Type) -> TypeModifier:
class RequiredModifier(Modifier): class RequiredModifier(Modifier):
def __init__(self) -> None: def __init__(self, type_modifier: Optional[TypeModifier] = None) -> None:
self.type_modifier = None self.type_modifier = type_modifier
def as_int(self) -> Self: def as_int(self) -> Self:
self.type_modifier = TypeModifier(int) self.type_modifier = TypeModifier(int)
@ -834,6 +904,11 @@ class RequiredModifier(Modifier):
provider = provider.as_(self.type_modifier.type_) provider = provider.as_(self.type_modifier.type_)
return provider return provider
def __repr__(self) -> str:
if self.type_modifier:
return f"{self.__class__.__name__}({self.type_modifier!r})"
return f"{self.__class__.__name__}()"
def required() -> RequiredModifier: def required() -> RequiredModifier:
"""Return required modifier.""" """Return required modifier."""
@ -853,6 +928,9 @@ class InvariantModifier(Modifier):
invariant_segment = providers_map.resolve_provider(self.id) invariant_segment = providers_map.resolve_provider(self.id)
return provider[invariant_segment] return provider[invariant_segment]
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.id!r})"
def invariant(id: str) -> InvariantModifier: def invariant(id: str) -> InvariantModifier:
"""Return invariant modifier.""" """Return invariant modifier."""
@ -893,8 +971,28 @@ class ProvidedInstance(Modifier):
provider = provider[value] provider = provider[value]
elif type_ == ProvidedInstance.TYPE_CALL: elif type_ == ProvidedInstance.TYPE_CALL:
provider = provider.call() provider = provider.call()
else:
assert_never(type_)
return provider return provider
def _format_segments(self) -> str:
segments = []
for type_, value in self.segments:
if type_ == ProvidedInstance.TYPE_ATTRIBUTE:
segments.append(f".{value}")
elif type_ == ProvidedInstance.TYPE_ITEM:
segments.append(f"[{value!r}]")
elif type_ == ProvidedInstance.TYPE_CALL:
segments.append(".call()")
else:
assert_never(type_)
return "".join(segments)
__str__ = _format_segments
def __repr__(self) -> str:
return f"{self.__class__.__name__}(){self._format_segments()}"
def provided() -> ProvidedInstance: def provided() -> ProvidedInstance:
"""Return provided instance modifier.""" """Return provided instance modifier."""
@ -910,7 +1008,7 @@ MarkerItem = Union[
] ]
if TYPE_CHECKING: if TYPE_CHECKING: # noqa
class _Marker(Protocol): class _Marker(Protocol):
__IS_MARKER__: bool __IS_MARKER__: bool
@ -918,6 +1016,7 @@ if TYPE_CHECKING:
def __call__(self) -> Self: ... def __call__(self) -> Self: ...
def __getattr__(self, item: str) -> Self: ... def __getattr__(self, item: str) -> Self: ...
def __getitem__(self, item: Any) -> Any: ... def __getitem__(self, item: Any) -> Any: ...
def __repr__(self) -> str: ...
Provide: _Marker Provide: _Marker
Provider: _Marker Provider: _Marker
@ -946,6 +1045,12 @@ else:
def __call__(self) -> Self: def __call__(self) -> Self:
return self return self
def __repr__(self) -> str:
cls_name = self.__class__.__name__
if self.modifier:
return f"{cls_name}[{self.provider!r}, {self.modifier!r}]"
return f"{cls_name}[{self.provider!r}]"
class Provide(_Marker): ... class Provide(_Marker): ...
class Provider(_Marker): ... class Provider(_Marker): ...

View File

@ -0,0 +1,15 @@
from dependency_injector.wiring import Provide, inject
missing_obj: object = Provide["missing"]
class TestMissingClass:
obj: object = Provide["missing"]
def method(self, obj: object = Provide["missing"]) -> object:
return obj
@inject
def test_missing_function(obj: object = Provide["missing"]):
return obj

View File

@ -1,5 +1,6 @@
"""Main wiring tests.""" """Main wiring tests."""
import re
from decimal import Decimal from decimal import Decimal
from dependency_injector import errors from dependency_injector import errors
@ -67,7 +68,7 @@ def test_module_attributes_wiring():
def test_module_attribute_wiring_with_invalid_marker(container: Container): def test_module_attribute_wiring_with_invalid_marker(container: Container):
from samples.wiring import module_invalid_attr_injection from samples.wiring import module_invalid_attr_injection
with raises(Exception, match="Unknown type of marker {0}".format(module_invalid_attr_injection.service)): with raises(Exception, match=re.escape("Unknown type of marker {0}".format(module_invalid_attr_injection.service))):
container.wire(modules=[module_invalid_attr_injection]) container.wire(modules=[module_invalid_attr_injection])

View File

@ -1,14 +1,21 @@
"""Main wiring tests.""" """Main wiring tests."""
import re
from decimal import Decimal from decimal import Decimal
from pytest import fixture, mark, raises from pytest import fixture, mark, raises, warns
from samples.wiringstringids import module, package, resourceclosing from samples.wiringstringids import module, package, resourceclosing
from samples.wiringstringids.container import Container, SubContainer from samples.wiringstringids.container import Container, SubContainer
from samples.wiringstringids.service import Service from samples.wiringstringids.service import Service
from dependency_injector import errors from dependency_injector import errors
from dependency_injector.wiring import Closing, Provide, Provider, wire from dependency_injector.wiring import (
Closing,
Provide,
Provider,
UnresolvedMarkerWarning,
wire,
)
@fixture(autouse=True) @fixture(autouse=True)
@ -68,10 +75,20 @@ def test_module_attributes_wiring():
def test_module_attribute_wiring_with_invalid_marker(container: Container): def test_module_attribute_wiring_with_invalid_marker(container: Container):
from samples.wiringstringids import module_invalid_attr_injection from samples.wiringstringids import module_invalid_attr_injection
with raises(Exception, match="Unknown type of marker {0}".format(module_invalid_attr_injection.service)): with raises(Exception, match=re.escape("Unknown type of marker {0}".format(module_invalid_attr_injection.service))):
container.wire(modules=[module_invalid_attr_injection]) container.wire(modules=[module_invalid_attr_injection])
def test_warn_unresolved_marker(container: Container):
from samples.wiringstringids import missing
with warns(
UnresolvedMarkerWarning,
match=r"^Unresolved marker .+ in .+$",
):
container.wire(modules=[missing], warn_unresolved=True)
def test_class_wiring(): def test_class_wiring():
test_class_object = module.TestClass() test_class_object = module.TestClass()
assert isinstance(test_class_object.service, Service) assert isinstance(test_class_object.service, Service)

View File

@ -0,0 +1,42 @@
from dependency_injector.wiring import (
Closing,
InvariantModifier,
Provide,
ProvidedInstance,
RequiredModifier,
TypeModifier,
)
def test_type_modifier_repr() -> None:
assert repr(TypeModifier(int)) == f"TypeModifier({int!r})"
def test_required_modifier_repr() -> None:
assert repr(RequiredModifier()) == "RequiredModifier()"
def test_required_modifier_with_type_repr() -> None:
type_modifier = TypeModifier(int)
required_modifier = RequiredModifier(type_modifier)
assert repr(required_modifier) == f"RequiredModifier({type_modifier!r})"
def test_invariant_modifier_repr() -> None:
assert repr(InvariantModifier("test")) == "InvariantModifier('test')"
def test_provided_instance_repr() -> None:
provided_instance = ProvidedInstance().test["attr"].call()
assert repr(provided_instance) == "ProvidedInstance().test['attr'].call()"
def test_marker_repr() -> None:
assert repr(Closing[Provide["test"]]) == "Closing[Provide['test']]"
def test_marker_with_modifier_repr() -> None:
marker = Provide["test", RequiredModifier()]
assert repr(marker) == "Provide['test', RequiredModifier()]"