diff --git a/scrapy_poet/injection.py b/scrapy_poet/injection.py index 7bac5b68..2e8189f0 100644 --- a/scrapy_poet/injection.py +++ b/scrapy_poet/injection.py @@ -20,7 +20,7 @@ from weakref import WeakKeyDictionary import andi -from andi.typeutils import issubclass_safe +from andi.typeutils import issubclass_safe, strip_annotated from scrapy import Request, Spider from scrapy.crawler import Crawler from scrapy.http import Response @@ -224,7 +224,7 @@ def _get_dynamic_deps_factory_text( # https://github.com/python/cpython/blob/v3.11.9/Lib/dataclasses.py#L413 args = [f"{name}_arg: {name}" for name in type_names] args_str = ", ".join(args) - result_args = [f"{name}: {name}_arg" for name in type_names] + result_args = [f"strip_annotated({name}): {name}_arg" for name in type_names] result_args_str = ", ".join(result_args) create_args_str = ", ".join(type_names) return ( @@ -245,12 +245,14 @@ def _get_dynamic_deps_factory( corresponding args. It has correct type hints so that it can be used as an ``andi`` custom builder. """ - ns: Dict[str, type] = {} + type_names: List[str] = [] for type_ in dynamic_types: + type_ = cast(type, strip_annotated(type_)) if not isinstance(type_, type): raise TypeError(f"Expected a dynamic dependency type, got {type_!r}") - ns[type_.__name__] = type_ - txt = Injector._get_dynamic_deps_factory_text(ns.keys()) + type_names.append(type_.__name__) + txt = Injector._get_dynamic_deps_factory_text(type_names) + ns: Dict[str, Any] = {} exec(txt, globals(), ns) return ns["__create_fn__"](*dynamic_types) diff --git a/tests/test_injection.py b/tests/test_injection.py index 90f03104..a2a216e5 100644 --- a/tests/test_injection.py +++ b/tests/test_injection.py @@ -666,6 +666,35 @@ def callback(dd: DynamicDeps): instances = yield from injector.build_instances(request, response, plan) assert set(instances) == {TestItemPage, TestItem, DynamicDeps} + @pytest.mark.skipif( + sys.version_info < (3, 9), reason="No Annotated support in Python < 3.9" + ) + @inlineCallbacks + def test_dynamic_deps_annotated(self): + from typing import Annotated + + def callback(dd: DynamicDeps): + pass + + provider = get_provider({Cls1, Cls2}) + injector = get_injector_for_testing({provider: 1}) + + expected_instances = { + DynamicDeps: DynamicDeps({Cls1: Cls1(), Cls2: Cls2()}), + Annotated[Cls1, 42]: Cls1(), + Annotated[Cls2, "foo"]: Cls2(), + } + expected_kwargs = { + "dd": DynamicDeps({Cls1: Cls1(), Cls2: Cls2()}), + } + yield self._assert_instances( + injector, + callback, + expected_instances, + expected_kwargs, + reqmeta={"inject": [Annotated[Cls1, 42], Annotated[Cls2, "foo"]]}, + ) + class Html(Injectable): url = "http://example.com" @@ -972,7 +1001,7 @@ def test_dynamic_deps_factory_text(): txt == """def __create_fn__(int, Cls1): def dynamic_deps_factory(int_arg: int, Cls1_arg: Cls1) -> DynamicDeps: - return DynamicDeps({int: int_arg, Cls1: Cls1_arg}) + return DynamicDeps({strip_annotated(int): int_arg, strip_annotated(Cls1): Cls1_arg}) return dynamic_deps_factory""" ) @@ -989,6 +1018,26 @@ def test_dynamic_deps_factory(): assert dd == {int: 42, Cls1: c} +@pytest.mark.skipif( + sys.version_info < (3, 9), reason="No Annotated support in Python < 3.9" +) +def test_dynamic_deps_factory_annotated(): + from typing import Annotated + + fn = Injector._get_dynamic_deps_factory( + [Annotated[Cls1, 42], Annotated[Cls2, "foo"]] + ) + args = andi.inspect(fn) + assert args == { + "Cls1_arg": [Annotated[Cls1, 42]], + "Cls2_arg": [Annotated[Cls2, "foo"]], + } + c1 = Cls1() + c2 = Cls2() + dd = fn(Cls1_arg=c1, Cls2_arg=c2) + assert dd == {Cls1: c1, Cls2: c2} + + def test_dynamic_deps_factory_bad_input(): with pytest.raises( TypeError,