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)
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 sources
@ -63,3 +67,6 @@ publish:
# Create and upload tag
git tag -a $(VERSION) -m 'version $(VERSION)'
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
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
------

View File

@ -33,7 +33,7 @@ factories:
- :ref:`factory-specialize-provided-type`
- :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:
.. 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:
...
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
---------------------------------------------------

View File

@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools", "Cython>=3.1.1"]
requires = ["setuptools", "Cython>=3.1.4"]
build-backend = "setuptools.build_meta"
[project]
@ -54,7 +54,7 @@ classifiers = [
dynamic = ["version"]
dependencies = [
# 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'",
]

View File

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

View File

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

View File

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

View File

@ -72,6 +72,7 @@ class Container:
modules: Optional[Iterable[Any]] = None,
packages: Optional[Iterable[Any]] = None,
from_package: Optional[str] = None,
warn_unresolved: bool = False,
) -> None: ...
def unwire(self) -> 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:
"""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.packages = [*packages] if packages else []
self.from_package = from_package
self.auto_wire = auto_wire
self.keep_cache = keep_cache
self.warn_unresolved = warn_unresolved
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:
@ -259,7 +275,14 @@ class DynamicContainer(Container):
"""Check if auto wiring is needed."""
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.
:rtype: None
@ -298,6 +321,7 @@ class DynamicContainer(Container):
modules=modules,
packages=packages,
keep_cache=keep_cache,
warn_unresolved=warn_unresolved,
)
if modules:

View File

@ -1599,7 +1599,7 @@ cdef class ConfigurationOption(Provider):
return self._root
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):
return self._name

View File

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

View File

@ -1,14 +1,21 @@
"""Main wiring tests."""
import re
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.container import Container, SubContainer
from samples.wiringstringids.service import Service
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)
@ -68,10 +75,20 @@ def test_module_attributes_wiring():
def test_module_attribute_wiring_with_invalid_marker(container: Container):
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])
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():
test_class_object = module.TestClass()
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()]"