Skip to content

Commit 1222827

Browse files
committed
Add warnings for unresolved markers
1 parent 759d89e commit 1222827

File tree

7 files changed

+165
-17
lines changed

7 files changed

+165
-17
lines changed

docs/wiring.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,32 @@ To inject a container use special identifier ``<container>``:
251251
def foo(container: Container = Provide["<container>"]) -> None:
252252
...
253253
254+
Caveats
255+
~~~~~~~
256+
257+
While using string identifiers you may not notice a typo in the identifier until the code is executed.
258+
In order to aid with catching such errors early, you may pass `warn_unresolved=True` to the ``wire`` method and/or :class:`WiringConfiguration`:
259+
260+
.. code-block:: python
261+
:emphasize-lines: 4
262+
263+
class Container(containers.DeclarativeContainer):
264+
wiring_config = containers.WiringConfiguration(
265+
modules=["yourapp.module"],
266+
warn_unresolved=True,
267+
)
268+
269+
Or:
270+
271+
.. code-block:: python
272+
:emphasize-lines: 4
273+
274+
container = Container()
275+
container.wire(
276+
modules=["yourapp.module"],
277+
warn_unresolved=True,
278+
)
279+
254280
255281
Making injections into modules and class attributes
256282
---------------------------------------------------

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ classifiers = [
5454
dynamic = ["version"]
5555
dependencies = [
5656
# typing.Annotated since v3.9
57-
# typing.Self since v3.11
57+
# typing.Self and typing.assert_never since v3.11
5858
"typing-extensions; python_version<'3.11'",
5959
]
6060

src/dependency_injector/containers.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class Container:
7272
modules: Optional[Iterable[Any]] = None,
7373
packages: Optional[Iterable[Any]] = None,
7474
from_package: Optional[str] = None,
75+
warn_unresolved: bool = False,
7576
) -> None: ...
7677
def unwire(self) -> None: ...
7778
def init_resources(self, resource_type: Type[Resource[Any]] = Resource) -> Optional[Awaitable[None]]: ...

src/dependency_injector/containers.pyx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,31 @@ from .wiring import wire, unwire
2020
class WiringConfiguration:
2121
"""Container wiring configuration."""
2222

23-
def __init__(self, modules=None, packages=None, from_package=None, auto_wire=True, keep_cache=False):
23+
def __init__(
24+
self,
25+
modules=None,
26+
packages=None,
27+
from_package=None,
28+
auto_wire=True,
29+
keep_cache=False,
30+
warn_unresolved=False,
31+
):
2432
self.modules = [*modules] if modules else []
2533
self.packages = [*packages] if packages else []
2634
self.from_package = from_package
2735
self.auto_wire = auto_wire
2836
self.keep_cache = keep_cache
37+
self.warn_unresolved = warn_unresolved
2938

3039
def __deepcopy__(self, memo=None):
31-
return self.__class__(self.modules, self.packages, self.from_package, self.auto_wire, self.keep_cache)
40+
return self.__class__(
41+
self.modules,
42+
self.packages,
43+
self.from_package,
44+
self.auto_wire,
45+
self.keep_cache,
46+
self.warn_unresolved,
47+
)
3248

3349

3450
class Container:
@@ -259,7 +275,14 @@ class DynamicContainer(Container):
259275
"""Check if auto wiring is needed."""
260276
return self.wiring_config.auto_wire is True
261277

262-
def wire(self, modules=None, packages=None, from_package=None, keep_cache=None):
278+
def wire(
279+
self,
280+
modules=None,
281+
packages=None,
282+
from_package=None,
283+
keep_cache=None,
284+
warn_unresolved=False,
285+
):
263286
"""Wire container providers with provided packages and modules.
264287
265288
:rtype: None
@@ -298,6 +321,7 @@ class DynamicContainer(Container):
298321
modules=modules,
299322
packages=packages,
300323
keep_cache=keep_cache,
324+
warn_unresolved=warn_unresolved,
301325
)
302326

303327
if modules:

src/dependency_injector/wiring.py

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,14 @@
2525
Type,
2626
TypeVar,
2727
Union,
28-
assert_never,
2928
cast,
3029
)
3130
from warnings import warn
3231

3332
try:
34-
from typing import Self
33+
from typing import Self, assert_never
3534
except ImportError:
36-
from typing_extensions import Self
35+
from typing_extensions import Self, assert_never
3736

3837
try:
3938
from functools import cache
@@ -140,6 +139,10 @@ class DIWiringWarning(RuntimeWarning):
140139
"""Base class for all warnings raised by the wiring module."""
141140

142141

142+
class UnresolvedMarkerWarning(DIWiringWarning):
143+
"""Warning raised when a marker with string identifier cannot be resolved against container."""
144+
145+
143146
class PatchedRegistry:
144147

145148
def __init__(self) -> None:
@@ -434,6 +437,7 @@ def wire( # noqa: C901
434437
modules: Optional[Iterable[ModuleType]] = None,
435438
packages: Optional[Iterable[ModuleType]] = None,
436439
keep_cache: bool = False,
440+
warn_unresolved: bool = False,
437441
) -> None:
438442
"""Wire container providers with provided packages and modules."""
439443
modules = [*modules] if modules else []
@@ -450,9 +454,23 @@ def wire( # noqa: C901
450454
continue
451455

452456
if _is_marker(member):
453-
_patch_attribute(module, member_name, member, providers_map)
457+
_patch_attribute(
458+
module,
459+
member_name,
460+
member,
461+
providers_map,
462+
warn_unresolved=warn_unresolved,
463+
warn_unresolved_stacklevel=1,
464+
)
454465
elif inspect.isfunction(member):
455-
_patch_fn(module, member_name, member, providers_map)
466+
_patch_fn(
467+
module,
468+
member_name,
469+
member,
470+
providers_map,
471+
warn_unresolved=warn_unresolved,
472+
warn_unresolved_stacklevel=1,
473+
)
456474
elif inspect.isclass(member):
457475
cls = member
458476
try:
@@ -464,15 +482,30 @@ def wire( # noqa: C901
464482
for cls_member_name, cls_member in cls_members:
465483
if _is_marker(cls_member):
466484
_patch_attribute(
467-
cls, cls_member_name, cls_member, providers_map
485+
cls,
486+
cls_member_name,
487+
cls_member,
488+
providers_map,
489+
warn_unresolved=warn_unresolved,
490+
warn_unresolved_stacklevel=1,
468491
)
469492
elif _is_method(cls_member):
470493
_patch_method(
471-
cls, cls_member_name, cls_member, providers_map
494+
cls,
495+
cls_member_name,
496+
cls_member,
497+
providers_map,
498+
warn_unresolved=warn_unresolved,
499+
warn_unresolved_stacklevel=1,
472500
)
473501

474502
for patched in _patched_registry.get_callables_from_module(module):
475-
_bind_injections(patched, providers_map)
503+
_bind_injections(
504+
patched,
505+
providers_map,
506+
warn_unresolved=warn_unresolved,
507+
warn_unresolved_stacklevel=1,
508+
)
476509

477510
if not keep_cache:
478511
clear_cache()
@@ -525,14 +558,21 @@ def _patch_fn(
525558
name: str,
526559
fn: Callable[..., Any],
527560
providers_map: ProvidersMap,
561+
warn_unresolved: bool = False,
562+
warn_unresolved_stacklevel: int = 0,
528563
) -> None:
529564
if not _is_patched(fn):
530565
reference_injections, reference_closing = _fetch_reference_injections(fn)
531566
if not reference_injections:
532567
return
533568
fn = _get_patched(fn, reference_injections, reference_closing)
534569

535-
_bind_injections(fn, providers_map)
570+
_bind_injections(
571+
fn,
572+
providers_map,
573+
warn_unresolved=warn_unresolved,
574+
warn_unresolved_stacklevel=warn_unresolved_stacklevel + 1,
575+
)
536576

537577
setattr(module, name, fn)
538578

@@ -542,6 +582,8 @@ def _patch_method(
542582
name: str,
543583
method: Callable[..., Any],
544584
providers_map: ProvidersMap,
585+
warn_unresolved: bool = False,
586+
warn_unresolved_stacklevel: int = 0,
545587
) -> None:
546588
if (
547589
hasattr(cls, "__dict__")
@@ -559,7 +601,12 @@ def _patch_method(
559601
return
560602
fn = _get_patched(fn, reference_injections, reference_closing)
561603

562-
_bind_injections(fn, providers_map)
604+
_bind_injections(
605+
fn,
606+
providers_map,
607+
warn_unresolved=warn_unresolved,
608+
warn_unresolved_stacklevel=warn_unresolved_stacklevel + 1,
609+
)
563610

564611
if fn is method:
565612
# Hotfix, see: https://github.com/ets-labs/python-dependency-injector/issues/884
@@ -595,9 +642,17 @@ def _patch_attribute(
595642
name: str,
596643
marker: "_Marker",
597644
providers_map: ProvidersMap,
645+
warn_unresolved: bool = False,
646+
warn_unresolved_stacklevel: int = 0,
598647
) -> None:
599648
provider = providers_map.resolve_provider(marker.provider, marker.modifier)
600649
if provider is None:
650+
if warn_unresolved:
651+
warn(
652+
f"Unresolved marker {name} in {member!r}",
653+
UnresolvedMarkerWarning,
654+
stacklevel=warn_unresolved_stacklevel + 2,
655+
)
601656
return
602657

603658
_patched_registry.register_attribute(PatchedAttribute(member, name, marker))
@@ -674,7 +729,12 @@ def _fetch_reference_injections( # noqa: C901
674729
return injections, closing
675730

676731

677-
def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> None:
732+
def _bind_injections(
733+
fn: Callable[..., Any],
734+
providers_map: ProvidersMap,
735+
warn_unresolved: bool = False,
736+
warn_unresolved_stacklevel: int = 0,
737+
) -> None:
678738
patched_callable = _patched_registry.get_callable(fn)
679739
if patched_callable is None:
680740
return
@@ -683,6 +743,12 @@ def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> Non
683743
provider = providers_map.resolve_provider(marker.provider, marker.modifier)
684744

685745
if provider is None:
746+
if warn_unresolved:
747+
warn(
748+
f"Unresolved marker {injection} in {fn.__qualname__}",
749+
UnresolvedMarkerWarning,
750+
stacklevel=warn_unresolved_stacklevel + 2,
751+
)
686752
continue
687753

688754
if isinstance(marker, Provide):
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from dependency_injector.wiring import Provide, inject
2+
3+
missing_obj: object = Provide["missing"]
4+
5+
6+
class TestMissingClass:
7+
obj: object = Provide["missing"]
8+
9+
def method(self, obj: object = Provide["missing"]) -> object:
10+
return obj
11+
12+
13+
@inject
14+
def test_missing_function(obj: object = Provide["missing"]):
15+
return obj

tests/unit/wiring/string_ids/test_main_py36.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@
33
import re
44
from decimal import Decimal
55

6-
from pytest import fixture, mark, raises
6+
from pytest import fixture, mark, raises, warns
77
from samples.wiringstringids import module, package, resourceclosing
88
from samples.wiringstringids.container import Container, SubContainer
99
from samples.wiringstringids.service import Service
1010

1111
from dependency_injector import errors
12-
from dependency_injector.wiring import Closing, Provide, Provider, wire
12+
from dependency_injector.wiring import (
13+
Closing,
14+
Provide,
15+
Provider,
16+
UnresolvedMarkerWarning,
17+
wire,
18+
)
1319

1420

1521
@fixture(autouse=True)
@@ -73,6 +79,16 @@ def test_module_attribute_wiring_with_invalid_marker(container: Container):
7379
container.wire(modules=[module_invalid_attr_injection])
7480

7581

82+
def test_warn_unresolved_marker(container: Container):
83+
from samples.wiringstringids import missing
84+
85+
with warns(
86+
UnresolvedMarkerWarning,
87+
match=r"^Unresolved marker .+ in .+$",
88+
):
89+
container.wire(modules=[missing], warn_unresolved=True)
90+
91+
7692
def test_class_wiring():
7793
test_class_object = module.TestClass()
7894
assert isinstance(test_class_object.service, Service)

0 commit comments

Comments
 (0)