diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ee4b77..438cdec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improve `resolve()` typing, by @sobolevn. - Use `Self` type for Container, by @sobolevn. - Improve typing of `inject`, by @sobolevn. +- Do not ignore _globalns that is set via `inject()`, by @sobolevn. Address an inconsistency: `inject(globalsns=...)` silently had no effect during class/init resolution even though the parameter was + stored — users passing a custom `globalsns` would get no error but also no result. The factory path already honoured it, so this brings the two resolution paths into + parity. - Drop support for Python <= 3.10. - Add Python 3.14 to the build matrix and to classifiers. - Remove Codecov from GitHub Workflow and from README. diff --git a/rodi/__init__.py b/rodi/__init__.py index 69ad324..a36aa88 100644 --- a/rodi/__init__.py +++ b/rodi/__init__.py @@ -84,6 +84,10 @@ def _get_obj_locals(obj) -> dict[str, Any] | None: return getattr(obj, "_locals", None) +def _get_obj_globals(obj) -> dict[str, Any]: + return getattr(obj, "_globals", {}) + + def class_name(input_type): if input_type in {list, set} and str( # noqa: E721 type(input_type) == "" @@ -568,9 +572,11 @@ def _resolve_by_init_method(self, context: ResolutionContext): for key, value in sig.parameters.items() } + globalns = dict(vars(sys.modules[self.concrete_type.__module__])) + globalns.update(_get_obj_globals(self.concrete_type)) annotations = get_type_hints( self.concrete_type.__init__, - vars(sys.modules[self.concrete_type.__module__]), + globalns, _get_obj_locals(self.concrete_type), ) for key, value in params.items(): @@ -646,9 +652,11 @@ def __call__(self, context: ResolutionContext): chain.append(concrete_type) if self._has_default_init(): + globalns = dict(vars(sys.modules[concrete_type.__module__])) + globalns.update(_get_obj_globals(concrete_type)) annotations = get_type_hints( concrete_type, - vars(sys.modules[concrete_type.__module__]), + globalns, _get_obj_locals(concrete_type), ) diff --git a/tests/test_services.py b/tests/test_services.py index a55c529..c2cb3bf 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -2760,3 +2760,51 @@ async def test_nested_scope_async_1(): nested_scope_async(), nested_scope_async(), ) + + +# Tests for inject(globalsns=...) being honoured during type resolution (#60) + + +def test_inject_globalsns_honoured_for_annotation_resolution(): + """ + When a class uses a forward reference in a class-level annotation and the + type is provided via inject(globalsns=...), it should be resolved correctly. + """ + + class LocalDep: + pass + + @inject(globalsns={"LocalDep": LocalDep}) + class Service: + dep: "LocalDep" + + container = Container() + container.add_transient(LocalDep) + container.add_transient(Service) + provider = container.build_provider() + + instance = provider.get(Service) + assert isinstance(instance.dep, LocalDep) + + +def test_inject_globalsns_honoured_for_init_resolution(): + """ + When a class uses a forward reference in __init__ and the type is provided + via inject(globalsns=...), it should be resolved correctly. + """ + + class LocalDep: + pass + + @inject(globalsns={"LocalDep": LocalDep}) + class Service: + def __init__(self, dep: "LocalDep") -> None: + self.dep = dep + + container = Container() + container.add_transient(LocalDep) + container.add_transient(Service) + provider = container.build_provider() + + instance = provider.get(Service) + assert isinstance(instance.dep, LocalDep)