From 62ca8c55095e2a6785c3d19d1c93d2d5ccd0451d Mon Sep 17 00:00:00 2001 From: nir Date: Sun, 26 Jan 2025 12:10:32 +0200 Subject: [PATCH 1/6] fix #23 --- aioinject/_features/generics.py | 43 +++++++++++++-- tests/features/test_generics.py | 96 +++++++++++++++++---------------- tests/utils_.py | 9 ++++ uv.lock | 6 +-- 4 files changed, 103 insertions(+), 51 deletions(-) diff --git a/aioinject/_features/generics.py b/aioinject/_features/generics.py index 9c7a262..6297cb0 100644 --- a/aioinject/_features/generics.py +++ b/aioinject/_features/generics.py @@ -1,6 +1,10 @@ from __future__ import annotations +from collections.abc import Callable import functools +import sys +from textwrap import wrap +import textwrap import types import typing as t from types import GenericAlias @@ -72,9 +76,42 @@ 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] = _py310_compat_resolve_generics( + inner_type, resolved_args + ) return result + + +def is_py_gt3_311() -> bool: + return sys.version_info >= (3, 11) + +def _py310_compat_resolve_generics_factory( +) -> Callable[[type, tuple[type, ...]], type]: + # we need to exec a string to avoid syntax errors + # we will create a function that will return the resolved generic + + if False: + fn_impl = textwrap.dedent(""" + def _resolve_generic( + generic_alias: type, + args: tuple[type, ...], + ) -> type: + return generic_alias[*args] + """) + else: + fn_impl = textwrap.dedent(""" + def _resolve_generic( + generic_alias: type, + args: tuple[type, ...], + ) -> type: + return generic_alias.__getitem__(*args) + """) + exec_globals = {} + exec(fn_impl, exec_globals) # noqa: S102 + return exec_globals["_resolve_generic"] + +_py310_compat_resolve_generics = _py310_compat_resolve_generics_factory() \ No newline at end of file diff --git a/tests/features/test_generics.py b/tests/features/test_generics.py index fdb6cd0..8bd67e4 100644 --- a/tests/features/test_generics.py +++ b/tests/features/test_generics.py @@ -1,47 +1,68 @@ import abc from collections.abc import Awaitable, Callable +from dataclasses import dataclass from typing import Generic, TypeVar import pytest from aioinject import Container, Object, Scoped -from aioinject.providers import Dependency, Transient +from aioinject.providers import Dependency, Singleton, Transient 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 +73,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,42 +92,27 @@ 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 - - +@pytest.mark.py_gte_311 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 + 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 class NestedUnresolvedGeneric(Generic[T]): - def __init__(self, service: WithGenericDependency[T]) -> None: + def __init__(self, service: SimpleGeneric[T]) -> None: self.service = service @@ -114,7 +120,7 @@ async def test_nested_unresolved_generic() -> None: container = Container() container.register( Scoped(NestedUnresolvedGeneric[int]), - Scoped(WithGenericDependency[int]), + Scoped(SimpleGeneric[int]), Object(42), Object("42"), ) @@ -122,7 +128,7 @@ async def test_nested_unresolved_generic() -> None: async with container.context() as ctx: instance = await ctx.resolve(NestedUnresolvedGeneric[int]) assert isinstance(instance, NestedUnresolvedGeneric) - assert isinstance(instance.service, WithGenericDependency) + assert isinstance(instance.service, SimpleGeneric) assert instance.service.dependency == MEANING_OF_LIFE_INT @@ -133,7 +139,7 @@ class GenericImpl(NestedUnresolvedGeneric[str]): container = Container() container.register( Scoped(GenericImpl), - Scoped(WithGenericDependency[str]), + Scoped(SimpleGeneric[str]), Object(42), Object("42"), ) @@ -141,7 +147,7 @@ class GenericImpl(NestedUnresolvedGeneric[str]): async with container.context() as ctx: instance = await ctx.resolve(GenericImpl) assert isinstance(instance, GenericImpl) - assert isinstance(instance.service, WithGenericDependency) + assert isinstance(instance.service, SimpleGeneric) assert instance.service.dependency == "42" @@ -150,7 +156,7 @@ async def test_partially_resolved_generic() -> None: class TwoGeneric(Generic[T, K]): def __init__( - self, a: WithGenericDependency[T], b: WithGenericDependency[K] + self, a: SimpleGeneric[T], b: SimpleGeneric[K] ) -> None: self.a = a self.b = b @@ -163,8 +169,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 +179,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/tests/utils_.py b/tests/utils_.py index c651665..ac66178 100644 --- a/tests/utils_.py +++ b/tests/utils_.py @@ -1,7 +1,10 @@ import functools from collections.abc import Callable +import sys from typing import ParamSpec, TypeVar +import pytest + T = TypeVar("T") P = ParamSpec("P") @@ -13,3 +16,9 @@ def decorator(*args: P.args, **kwargs: P.kwargs) -> T: return func(*args, **kwargs) return decorator + + +py_gte_311 = pytest.mark.skipif( + sys.version_info < (3, 11), + reason="This test requires Python 3.11 or later", +) diff --git a/uv.lock b/uv.lock index 53f1390..62a6df4 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" }, @@ -343,7 +343,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 +751,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" }, From 92ff83bb669775be0a7565be28fee61f449357c4 Mon Sep 17 00:00:00 2001 From: nir Date: Sun, 26 Jan 2025 12:12:01 +0200 Subject: [PATCH 2/6] fix: update compatibility check for Python version greater than 3.11 --- aioinject/_features/generics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aioinject/_features/generics.py b/aioinject/_features/generics.py index 6297cb0..8f82b55 100644 --- a/aioinject/_features/generics.py +++ b/aioinject/_features/generics.py @@ -94,7 +94,7 @@ def _py310_compat_resolve_generics_factory( # we need to exec a string to avoid syntax errors # we will create a function that will return the resolved generic - if False: + if is_py_gt3_311: fn_impl = textwrap.dedent(""" def _resolve_generic( generic_alias: type, From 369d724bef7fd544fe5355cb668370445415c7a6 Mon Sep 17 00:00:00 2001 From: nir Date: Sun, 26 Jan 2025 12:15:32 +0200 Subject: [PATCH 3/6] docs: add comments for Python 3.11 compatibility in generics factory --- aioinject/_features/generics.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aioinject/_features/generics.py b/aioinject/_features/generics.py index 8f82b55..6dfa4d3 100644 --- a/aioinject/_features/generics.py +++ b/aioinject/_features/generics.py @@ -93,6 +93,8 @@ def _py310_compat_resolve_generics_factory( ) -> Callable[[type, tuple[type, ...]], type]: # we need to exec a string to avoid syntax errors # we will create a function that will return the resolved generic + # for python 3.11 and later we can use `generic_alias[*args]` which will consider + # see `test_partially_resolved_generic` for more details if is_py_gt3_311: fn_impl = textwrap.dedent(""" From a88ff3fc08149112648aa6217e6edf31396c7572 Mon Sep 17 00:00:00 2001 From: nir Date: Sun, 26 Jan 2025 12:22:43 +0200 Subject: [PATCH 4/6] refactor: improve compatibility handling and clean up generics code --- aioinject/_features/generics.py | 12 +++++---- tests/features/test_generics.py | 45 +++++++++------------------------ tests/utils_.py | 2 +- 3 files changed, 20 insertions(+), 39 deletions(-) diff --git a/aioinject/_features/generics.py b/aioinject/_features/generics.py index 6dfa4d3..539a21c 100644 --- a/aioinject/_features/generics.py +++ b/aioinject/_features/generics.py @@ -1,12 +1,11 @@ from __future__ import annotations -from collections.abc import Callable import functools import sys -from textwrap import wrap import textwrap import types import typing as t +from collections.abc import Callable from types import GenericAlias from typing import TYPE_CHECKING, Any, TypeGuard @@ -89,8 +88,10 @@ def get_generic_parameter_map( def is_py_gt3_311() -> bool: return sys.version_info >= (3, 11) -def _py310_compat_resolve_generics_factory( -) -> Callable[[type, tuple[type, ...]], type]: + +def _py310_compat_resolve_generics_factory() -> ( + Callable[[type, tuple[type, ...]], type] +): # we need to exec a string to avoid syntax errors # we will create a function that will return the resolved generic # for python 3.11 and later we can use `generic_alias[*args]` which will consider @@ -116,4 +117,5 @@ def _resolve_generic( exec(fn_impl, exec_globals) # noqa: S102 return exec_globals["_resolve_generic"] -_py310_compat_resolve_generics = _py310_compat_resolve_generics_factory() \ No newline at end of file + +_py310_compat_resolve_generics = _py310_compat_resolve_generics_factory() diff --git a/tests/features/test_generics.py b/tests/features/test_generics.py index 8bd67e4..0357142 100644 --- a/tests/features/test_generics.py +++ b/tests/features/test_generics.py @@ -6,7 +6,7 @@ import pytest from aioinject import Container, Object, Scoped -from aioinject.providers import Dependency, Singleton, Transient +from aioinject.providers import Dependency, Transient T = TypeVar("T") @@ -34,11 +34,13 @@ 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 @@ -111,53 +113,30 @@ async def test_nested_generics() -> None: assert instance.u == MEANING_OF_LIFE_STR -class NestedUnresolvedGeneric(Generic[T]): - def __init__(self, service: SimpleGeneric[T]) -> None: - self.service = service - - async def test_nested_unresolved_generic() -> None: + @dataclass + class NestedUnresolvedGeneric: + service: SimpleGeneric + container = Container() + obj = SimpleGeneric(MEANING_OF_LIFE_INT) container.register( - Scoped(NestedUnresolvedGeneric[int]), - Scoped(SimpleGeneric[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, 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(SimpleGeneric[str]), - Object(42), - Object("42"), - ) - - async with container.context() as ctx: - instance = await ctx.resolve(GenericImpl) - assert isinstance(instance, GenericImpl) - assert isinstance(instance.service, SimpleGeneric) - assert instance.service.dependency == "42" - - async def test_partially_resolved_generic() -> None: K = TypeVar("K") class TwoGeneric(Generic[T, K]): - def __init__( - self, a: SimpleGeneric[T], b: SimpleGeneric[K] - ) -> None: + def __init__(self, a: SimpleGeneric[T], b: SimpleGeneric[K]) -> None: self.a = a self.b = b diff --git a/tests/utils_.py b/tests/utils_.py index ac66178..8f5a88d 100644 --- a/tests/utils_.py +++ b/tests/utils_.py @@ -1,6 +1,6 @@ import functools -from collections.abc import Callable import sys +from collections.abc import Callable from typing import ParamSpec, TypeVar import pytest From de411b3f3b65724b11ded7812c94f1c335818b51 Mon Sep 17 00:00:00 2001 From: nir Date: Sun, 26 Jan 2025 12:30:45 +0200 Subject: [PATCH 5/6] refactor: enhance Python 3.11 compatibility checks and update test decorators --- aioinject/_features/generics.py | 8 ++++---- pyproject.toml | 1 + tests/features/test_generics.py | 8 ++++++-- tests/utils_.py | 9 +++++---- uv.lock | 15 +++++++++++++++ 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/aioinject/_features/generics.py b/aioinject/_features/generics.py index 539a21c..b7f10e0 100644 --- a/aioinject/_features/generics.py +++ b/aioinject/_features/generics.py @@ -85,19 +85,19 @@ def get_generic_parameter_map( return result -def is_py_gt3_311() -> bool: +def is_py_gt3_311() -> bool: # pragma: no cover return sys.version_info >= (3, 11) def _py310_compat_resolve_generics_factory() -> ( Callable[[type, tuple[type, ...]], type] -): +): # pragma: no cover # we need to exec a string to avoid syntax errors # we will create a function that will return the resolved generic # for python 3.11 and later we can use `generic_alias[*args]` which will consider # see `test_partially_resolved_generic` for more details - if is_py_gt3_311: + if is_py_gt3_311(): fn_impl = textwrap.dedent(""" def _resolve_generic( generic_alias: type, @@ -113,7 +113,7 @@ def _resolve_generic( ) -> type: return generic_alias.__getitem__(*args) """) - exec_globals = {} + exec_globals: dict[str, Any] = {} exec(fn_impl, exec_globals) # noqa: S102 return exec_globals["_resolve_generic"] 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 0357142..3da3977 100644 --- a/tests/features/test_generics.py +++ b/tests/features/test_generics.py @@ -7,6 +7,7 @@ from aioinject import Container, Object, Scoped from aioinject.providers import Dependency, Transient +from tests.utils_ import py_gte_311 T = TypeVar("T") @@ -94,7 +95,10 @@ async def test_resolve_generics( assert isinstance(instance, instanceof) -@pytest.mark.py_gte_311 +@py_gte_311(""" + prior to 3.11 using generic_alias[] considered a syntax error + its very hard to overcome this due to `test_partially_resolved_generic` + """) async def test_nested_generics() -> None: container = Container() container.register( @@ -116,7 +120,7 @@ async def test_nested_generics() -> None: async def test_nested_unresolved_generic() -> None: @dataclass class NestedUnresolvedGeneric: - service: SimpleGeneric + service: SimpleGeneric # type: ignore[type-arg] container = Container() obj = SimpleGeneric(MEANING_OF_LIFE_INT) diff --git a/tests/utils_.py b/tests/utils_.py index 8f5a88d..dc7f294 100644 --- a/tests/utils_.py +++ b/tests/utils_.py @@ -18,7 +18,8 @@ def decorator(*args: P.args, **kwargs: P.kwargs) -> T: return decorator -py_gte_311 = pytest.mark.skipif( - sys.version_info < (3, 11), - reason="This test requires Python 3.11 or later", -) +def py_gte_311(reason: str) -> pytest.MarkDecorator: + return pytest.mark.skipif( + sys.version_info < (3, 11), + reason=reason, + ) diff --git a/uv.lock b/uv.lock index 62a6df4..76397df 100644 --- a/uv.lock +++ b/uv.lock @@ -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" }, @@ -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" From f0d622dc6b90605f3110ef6992e5eed8294088c7 Mon Sep 17 00:00:00 2001 From: Doctor Date: Wed, 5 Feb 2025 13:43:13 +0300 Subject: [PATCH 6/6] refactor(generics): try using Generic[args] to make generics --- aioinject/_features/generics.py | 43 +-------------------------------- tests/features/test_generics.py | 5 ---- tests/utils_.py | 10 -------- 3 files changed, 1 insertion(+), 57 deletions(-) diff --git a/aioinject/_features/generics.py b/aioinject/_features/generics.py index b7f10e0..228078c 100644 --- a/aioinject/_features/generics.py +++ b/aioinject/_features/generics.py @@ -1,11 +1,8 @@ from __future__ import annotations import functools -import sys -import textwrap import types import typing as t -from collections.abc import Callable from types import GenericAlias from typing import TYPE_CHECKING, Any, TypeGuard @@ -79,43 +76,5 @@ def get_generic_parameter_map( args_map[arg.__name__] for arg in generic_arguments ) # We can use `[]` when we drop support for 3.10 - result[dependency.name] = _py310_compat_resolve_generics( - inner_type, resolved_args - ) + result[dependency.name] = inner_type[resolved_args] return result - - -def is_py_gt3_311() -> bool: # pragma: no cover - return sys.version_info >= (3, 11) - - -def _py310_compat_resolve_generics_factory() -> ( - Callable[[type, tuple[type, ...]], type] -): # pragma: no cover - # we need to exec a string to avoid syntax errors - # we will create a function that will return the resolved generic - # for python 3.11 and later we can use `generic_alias[*args]` which will consider - # see `test_partially_resolved_generic` for more details - - if is_py_gt3_311(): - fn_impl = textwrap.dedent(""" - def _resolve_generic( - generic_alias: type, - args: tuple[type, ...], - ) -> type: - return generic_alias[*args] - """) - else: - fn_impl = textwrap.dedent(""" - def _resolve_generic( - generic_alias: type, - args: tuple[type, ...], - ) -> type: - return generic_alias.__getitem__(*args) - """) - exec_globals: dict[str, Any] = {} - exec(fn_impl, exec_globals) # noqa: S102 - return exec_globals["_resolve_generic"] - - -_py310_compat_resolve_generics = _py310_compat_resolve_generics_factory() diff --git a/tests/features/test_generics.py b/tests/features/test_generics.py index 3da3977..e328480 100644 --- a/tests/features/test_generics.py +++ b/tests/features/test_generics.py @@ -7,7 +7,6 @@ from aioinject import Container, Object, Scoped from aioinject.providers import Dependency, Transient -from tests.utils_ import py_gte_311 T = TypeVar("T") @@ -95,10 +94,6 @@ async def test_resolve_generics( assert isinstance(instance, instanceof) -@py_gte_311(""" - prior to 3.11 using generic_alias[] considered a syntax error - its very hard to overcome this due to `test_partially_resolved_generic` - """) async def test_nested_generics() -> None: container = Container() container.register( diff --git a/tests/utils_.py b/tests/utils_.py index dc7f294..c651665 100644 --- a/tests/utils_.py +++ b/tests/utils_.py @@ -1,10 +1,7 @@ import functools -import sys from collections.abc import Callable from typing import ParamSpec, TypeVar -import pytest - T = TypeVar("T") P = ParamSpec("P") @@ -16,10 +13,3 @@ def decorator(*args: P.args, **kwargs: P.kwargs) -> T: return func(*args, **kwargs) return decorator - - -def py_gte_311(reason: str) -> pytest.MarkDecorator: - return pytest.mark.skipif( - sys.version_info < (3, 11), - reason=reason, - )