diff --git a/aioinject/_features/generics.py b/aioinject/_features/generics.py index 9c7a262..228078c 100644 --- a/aioinject/_features/generics.py +++ b/aioinject/_features/generics.py @@ -72,9 +72,9 @@ def get_generic_parameter_map( ): # This is a generic type, we need to resolve the type arguments # and pass them to the provider. - resolved_args = [ + resolved_args = tuple( args_map[arg.__name__] for arg in generic_arguments - ] + ) # We can use `[]` when we drop support for 3.10 - result[dependency.name] = inner_type.__getitem__(*resolved_args) + result[dependency.name] = inner_type[resolved_args] return result diff --git a/pyproject.toml b/pyproject.toml index ff8c215..a68cf18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dev = [ "mkdocs-material>=9.5.44", "mypy>=1.13.0", "pydantic-settings>=2.6.1", + "pytest-cov>=6.0.0", "pytest>=8.3.3", "ruff>=0.7.4", "strawberry-graphql>=0.248.1", diff --git a/tests/features/test_generics.py b/tests/features/test_generics.py index fdb6cd0..e328480 100644 --- a/tests/features/test_generics.py +++ b/tests/features/test_generics.py @@ -1,5 +1,6 @@ import abc from collections.abc import Awaitable, Callable +from dataclasses import dataclass from typing import Generic, TypeVar import pytest @@ -9,39 +10,61 @@ T = TypeVar("T") +U = TypeVar("U") ReqT = TypeVar("ReqT") ResT = TypeVar("ResT") -class GenericService(Generic[T]): +class UnusedGeneric(Generic[T]): def __init__(self, dependency: str) -> None: self.dependency = dependency -class WithGenericDependency(Generic[T]): +class SimpleGeneric(Generic[T]): def __init__(self, dependency: T) -> None: self.dependency = dependency -class ConstrainedGenericDependency(WithGenericDependency[int]): +class SimpleGenericExtended(SimpleGeneric[int]): pass +class MultipleSimpleGeneric(Generic[T, U]): + def __init__(self, service: T, b: U) -> None: + self.service = service + self.b = b + + +class NestedGeneric(Generic[T, U]): + def __init__(self, simple_gen: MultipleSimpleGeneric[T, U], u: U) -> None: + self.simple_gen = simple_gen + self.u = u + + +class Something: + def __init__(self) -> None: + self.a = MEANING_OF_LIFE_INT + + +MEANING_OF_LIFE_INT = 42 +MEANING_OF_LIFE_STR = "42" + + async def test_generic_dependency() -> None: - assert Scoped(GenericService[int]).collect_dependencies() == ( + assert Scoped(UnusedGeneric[int]).collect_dependencies() == ( Dependency( name="dependency", type_=str, ), ) - assert Scoped(WithGenericDependency[int]).collect_dependencies() == ( + assert Scoped(SimpleGeneric[int]).collect_dependencies() == ( Dependency( name="dependency", type_=int, ), ) - assert Scoped(ConstrainedGenericDependency).collect_dependencies() == ( + assert Scoped(SimpleGenericExtended).collect_dependencies() == ( Dependency( name="dependency", type_=int, @@ -52,9 +75,9 @@ async def test_generic_dependency() -> None: @pytest.mark.parametrize( ("type_", "instanceof"), [ - (GenericService, GenericService), - (WithGenericDependency[int], WithGenericDependency), - (ConstrainedGenericDependency, ConstrainedGenericDependency), + (UnusedGeneric, UnusedGeneric), + (SimpleGeneric[int], SimpleGeneric), + (SimpleGenericExtended, SimpleGenericExtended), ], ) async def test_resolve_generics( @@ -71,87 +94,48 @@ async def test_resolve_generics( assert isinstance(instance, instanceof) -class NestedGenericService(Generic[T]): - def __init__(self, service: T) -> None: - self.service = service - - -MEANING_OF_LIFE_INT = 42 -MEANING_OF_LIFE_STR = "42" - - -class Something: - def __init__(self) -> None: - self.a = MEANING_OF_LIFE_INT - - async def test_nested_generics() -> None: container = Container() container.register( - Scoped(NestedGenericService[WithGenericDependency[Something]]), - Scoped(WithGenericDependency[Something]), - Scoped(Something), + Scoped(NestedGeneric[int, str]), + Scoped(MultipleSimpleGeneric[int, str]), Object(MEANING_OF_LIFE_INT), - Object("42"), + Object(MEANING_OF_LIFE_STR), ) async with container.context() as ctx: - instance = await ctx.resolve( - NestedGenericService[WithGenericDependency[Something]] - ) - assert isinstance(instance, NestedGenericService) - assert isinstance(instance.service, WithGenericDependency) - assert isinstance(instance.service.dependency, Something) - assert instance.service.dependency.a == MEANING_OF_LIFE_INT - - -class NestedUnresolvedGeneric(Generic[T]): - def __init__(self, service: WithGenericDependency[T]) -> None: - self.service = service + instance = await ctx.resolve(NestedGeneric[int, str]) + assert isinstance(instance, NestedGeneric) + assert isinstance(instance.simple_gen, MultipleSimpleGeneric) + assert instance.simple_gen.service == MEANING_OF_LIFE_INT + assert instance.simple_gen.b == MEANING_OF_LIFE_STR + assert instance.u == MEANING_OF_LIFE_STR async def test_nested_unresolved_generic() -> None: + @dataclass + class NestedUnresolvedGeneric: + service: SimpleGeneric # type: ignore[type-arg] + container = Container() + obj = SimpleGeneric(MEANING_OF_LIFE_INT) container.register( - Scoped(NestedUnresolvedGeneric[int]), - Scoped(WithGenericDependency[int]), - Object(42), - Object("42"), + Scoped(NestedUnresolvedGeneric), + Object(obj, type_=SimpleGeneric), ) async with container.context() as ctx: - instance = await ctx.resolve(NestedUnresolvedGeneric[int]) + instance = await ctx.resolve(NestedUnresolvedGeneric) assert isinstance(instance, NestedUnresolvedGeneric) - assert isinstance(instance.service, WithGenericDependency) + assert isinstance(instance.service, SimpleGeneric) assert instance.service.dependency == MEANING_OF_LIFE_INT -async def test_nested_unresolved_concrete_generic() -> None: - class GenericImpl(NestedUnresolvedGeneric[str]): - pass - - container = Container() - container.register( - Scoped(GenericImpl), - Scoped(WithGenericDependency[str]), - Object(42), - Object("42"), - ) - - async with container.context() as ctx: - instance = await ctx.resolve(GenericImpl) - assert isinstance(instance, GenericImpl) - assert isinstance(instance.service, WithGenericDependency) - assert instance.service.dependency == "42" - - async def test_partially_resolved_generic() -> None: K = TypeVar("K") class TwoGeneric(Generic[T, K]): - def __init__( - self, a: WithGenericDependency[T], b: WithGenericDependency[K] - ) -> None: + def __init__(self, a: SimpleGeneric[T], b: SimpleGeneric[K]) -> None: self.a = a self.b = b @@ -163,8 +147,8 @@ def __init__(self, service: TwoGeneric[T, str]) -> None: container.register( Scoped(UsesTwoGeneric[int]), Scoped(TwoGeneric[int, str]), - Scoped(WithGenericDependency[int]), - Scoped(WithGenericDependency[str]), + Scoped(SimpleGeneric[int]), + Scoped(SimpleGeneric[str]), Object(MEANING_OF_LIFE_INT), Object("42"), ) @@ -173,8 +157,8 @@ def __init__(self, service: TwoGeneric[T, str]) -> None: instance = await ctx.resolve(UsesTwoGeneric[int]) assert isinstance(instance, UsesTwoGeneric) assert isinstance(instance.service, TwoGeneric) - assert isinstance(instance.service.a, WithGenericDependency) - assert isinstance(instance.service.b, WithGenericDependency) + assert isinstance(instance.service.a, SimpleGeneric) + assert isinstance(instance.service.b, SimpleGeneric) assert instance.service.a.dependency == MEANING_OF_LIFE_INT assert instance.service.b.dependency == MEANING_OF_LIFE_STR diff --git a/uv.lock b/uv.lock index 53f1390..76397df 100644 --- a/uv.lock +++ b/uv.lock @@ -119,7 +119,7 @@ wheels = [ [[package]] name = "aioinject" -version = "0.35.3" +version = "0.36.0" source = { editable = "." } dependencies = [ { name = "typing-extensions" }, @@ -138,6 +138,7 @@ dev = [ { name = "mypy" }, { name = "pydantic-settings" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "ruff" }, { name = "strawberry-graphql" }, { name = "trio" }, @@ -160,6 +161,7 @@ dev = [ { name = "mypy", specifier = ">=1.13.0" }, { name = "pydantic-settings", specifier = ">=2.6.1" }, { name = "pytest", specifier = ">=8.3.3" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "ruff", specifier = ">=0.7.4" }, { name = "strawberry-graphql", specifier = ">=0.248.1" }, { name = "trio", specifier = ">=0.27.0" }, @@ -343,7 +345,7 @@ name = "click" version = "8.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } wheels = [ @@ -751,7 +753,7 @@ version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, { name = "ghp-import" }, { name = "jinja2" }, { name = "markdown" }, @@ -1243,6 +1245,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, ] +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0"