Repository: reagento/dishka
Pull request #51 (2024-02-16 23:40:29)

support concrete generics


Patch:
diff --git a/.coveragerc b/.coveragerc
index b2c41d0..0c1017c 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -2,3 +2,6 @@
 exclude_lines = 
     pragma: not covered
     @overload
+[run]
+omit =
+    src/dishka/_adaptix/**
\ No newline at end of file
diff --git a/.ruff.toml b/.ruff.toml
index 54e9ec4..f808599 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -1,8 +1,9 @@
 line-length = 79
 src = ["src"]
 include = ["src/**.py", "tests/**.py"]
+exclude = ["src/dishka/_adaptix/**"]
 
-lint.select = [
+select = [
     "E",
     "F",
     "BLE",
@@ -19,8 +20,8 @@ lint.select = [
 ]
 
 
-[lint.isort]
+[isort]
 no-lines-before = ["local-folder"]
 
-[lint.flake8-tidy-imports]
+[flake8-tidy-imports]
 ban-relative-imports = "parents"
\ No newline at end of file
diff --git a/src/dishka/_adaptix/common.py b/src/dishka/_adaptix/common.py
new file mode 100644
index 0000000..023a3fc
--- /dev/null
+++ b/src/dishka/_adaptix/common.py
@@ -0,0 +1,22 @@
+from typing import TYPE_CHECKING, Any, Callable, Tuple, Type, TypeVar, Union
+
+K_contra = TypeVar('K_contra', contravariant=True)
+V_co = TypeVar('V_co', covariant=True)
+T = TypeVar('T')
+
+Loader = Callable[[Any], V_co]
+Dumper = Callable[[K_contra], Any]
+Converter = Callable[..., Any]
+Coercer = Callable[[Any], Any]
+
+TypeHint = Any
+
+VarTuple = Tuple[T, ...]
+
+Catchable = Union[Type[BaseException], VarTuple[Type[BaseException]]]
+
+# https://github.com/python/typing/issues/684#issuecomment-548203158
+if TYPE_CHECKING:
+    EllipsisType = ellipsis  # pylint: disable=undefined-variable  # noqa: F821
+else:
+    EllipsisType = type(Ellipsis)
diff --git a/src/dishka/_adaptix/feature_requirement.py b/src/dishka/_adaptix/feature_requirement.py
new file mode 100644
index 0000000..bdb1a1d
--- /dev/null
+++ b/src/dishka/_adaptix/feature_requirement.py
@@ -0,0 +1,178 @@
+import importlib.metadata
+import itertools
+import re
+import sys
+from abc import ABC, abstractmethod
+from typing import Any, Iterable
+
+from .common import VarTuple
+
+
+def _true():
+    return True
+
+
+def _false():
+    return False
+
+
+class Requirement(ABC):
+    __slots__ = ('is_meet', '__bool__', '__dict__')
+
+    def __init__(self):
+        self.is_meet = self._evaluate()
+        self.__bool__ = _true if self.is_meet else _false
+
+    @abstractmethod
+    def _evaluate(self) -> bool:
+        ...
+
+    @property
+    @abstractmethod
+    def fail_reason(self) -> str:
+        ...
+
+
+class PythonVersionRequirement(Requirement):
+    def __init__(self, min_version: VarTuple[int]):
+        self.min_version = min_version
+        super().__init__()
+
+    def _evaluate(self) -> bool:
+        return sys.version_info >= self.min_version
+
+    @property
+    def fail_reason(self) -> str:
+        return f'Python >= {".".join(map(str, self.min_version))} is required'
+
+
+class DistributionRequirement(Requirement):
+    def __init__(self, distribution_name: str):
+        self.distribution_name = distribution_name
+        super().__init__()
+
+    def _evaluate(self) -> bool:
+        try:
+            importlib.metadata.distribution(self.distribution_name)
+        except importlib.metadata.PackageNotFoundError:
+            return False
+        return True
+
+    @property
+    def fail_reason(self) -> str:
+        return f'Installed distribution {self.distribution_name!r} is required'
+
+
+class DistributionVersionRequirement(DistributionRequirement):
+    # Pattern from PEP 440
+    _VERSION_PATTERN = r"""
+        v?
+        (?:
+            (?:(?P<epoch>[0-9]+)!)?                           # epoch
+            (?P<release>[0-9]+(?:\.[0-9]+)*)                  # release segment
+            (?P<pre>                                          # pre-release
+                [-_\.]?
+                (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
+                [-_\.]?
+                (?P<pre_n>[0-9]+)?
+            )?
+            (?P<post>                                         # post release
+                (?:-(?P<post_n1>[0-9]+))
+                |
+                (?:
+                    [-_\.]?
+                    (?P<post_l>post|rev|r)
+                    [-_\.]?
+                    (?P<post_n2>[0-9]+)?
+                )
+            )?
+            (?P<dev>                                          # dev release
+                [-_\.]?
+                (?P<dev_l>dev)
+                [-_\.]?
+                (?P<dev_n>[0-9]+)?
+            )?
+        )
+        (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+    """
+
+    _version_regex = re.compile(
+        r"^\s*" + _VERSION_PATTERN + r"\s*$",
+        re.VERBOSE | re.IGNORECASE,
+    )
+
+    def __init__(self, distribution_name: str, min_version: str):
+        self.min_version = min_version
+        super().__init__(distribution_name)
+
+    def _evaluate(self) -> bool:
+        try:
+            distribution = importlib.metadata.distribution(self.distribution_name)
+        except importlib.metadata.PackageNotFoundError:
+            return False
+        return (
+            self._make_comparator(distribution.version)
+            >=
+            self._make_comparator(self.min_version)
+        )
+
+    def _remove_trailing_zeros(self, data: Iterable[int]) -> VarTuple[int]:
+        return tuple(reversed(tuple(itertools.dropwhile(lambda x: x == 0, reversed(tuple(data))))))
+
+    def _make_comparator(self, version: str) -> VarTuple[Any]:
+        match = self._version_regex.match(version)
+        if match is None:
+            raise ValueError
+        return (
+            match.group('epoch') or 0,
+            self._remove_trailing_zeros(int(i) for i in match.group('release').split('.')),
+            match.group('pre') is not None,
+            match.group('dev') is not None,
+        )
+
+    @property
+    def fail_reason(self) -> str:
+        return f'Installed distribution {self.distribution_name!r} of {self.min_version!r} is required'
+
+
+class PythonImplementationRequirement(Requirement):
+    def __init__(self, implementation_name: str):
+        self.implementation_name = implementation_name
+        super().__init__()
+
+    def _evaluate(self) -> bool:
+        return sys.implementation.name == self.implementation_name
+
+    @property
+    def fail_reason(self) -> str:
+        return f'{self.implementation_name} is required'
+
+
+HAS_PY_39 = PythonVersionRequirement((3, 9))
+HAS_ANNOTATED = HAS_PY_39
+HAS_STD_CLASSES_GENERICS = HAS_PY_39
+
+HAS_PY_310 = PythonVersionRequirement((3, 10))
+HAS_TYPE_UNION_OP = HAS_PY_310
+HAS_TYPE_GUARD = HAS_PY_310
+HAS_TYPE_ALIAS = HAS_PY_310
+HAS_PARAM_SPEC = HAS_PY_310
+
+HAS_PY_311 = PythonVersionRequirement((3, 11))
+HAS_NATIVE_EXC_GROUP = HAS_PY_311
+HAS_TYPED_DICT_REQUIRED = HAS_PY_311
+HAS_SELF_TYPE = HAS_PY_311
+HAS_TV_TUPLE = HAS_PY_311
+HAS_UNPACK = HAS_PY_311
+
+HAS_PY_312 = PythonVersionRequirement((3, 12))
+HAS_TV_SYNTAX = HAS_PY_312
+
+HAS_SUPPORTED_ATTRS_PKG = DistributionVersionRequirement('attrs', '21.3.0')
+HAS_ATTRS_PKG = DistributionRequirement('attrs')
+
+HAS_SUPPORTED_SQLALCHEMY_PKG = DistributionVersionRequirement('sqlalchemy', '2.0.0')
+HAS_SQLALCHEMY_PKG = DistributionRequirement('sqlalchemy')
+
+IS_CPYTHON = PythonImplementationRequirement('cpython')
+IS_PYPY = PythonImplementationRequirement('pypy')
diff --git a/src/dishka/_adaptix/type_tools/__init__.py b/src/dishka/_adaptix/type_tools/__init__.py
new file mode 100644
index 0000000..63df61d
--- /dev/null
+++ b/src/dishka/_adaptix/type_tools/__init__.py
@@ -0,0 +1,26 @@
+from .basic_utils import (
+    create_union,
+    get_all_type_hints,
+    is_bare_generic,
+    is_generic,
+    is_generic_class,
+    is_named_tuple_class,
+    is_new_type,
+    is_parametrized,
+    is_protocol,
+    is_subclass_soft,
+    is_typed_dict_class,
+    is_user_defined_generic,
+    strip_alias,
+)
+from .norm_utils import is_class_var, strip_tags
+from .normalize_type import (
+    AnyNormTypeVarLike,
+    BaseNormType,
+    NormParamSpecMarker,
+    NormTV,
+    NormTVTuple,
+    NormTypeAlias,
+    make_norm_type,
+    normalize_type,
+)
diff --git a/src/dishka/_adaptix/type_tools/basic_utils.py b/src/dishka/_adaptix/type_tools/basic_utils.py
new file mode 100644
index 0000000..877e3ef
--- /dev/null
+++ b/src/dishka/_adaptix/type_tools/basic_utils.py
@@ -0,0 +1,180 @@
+import types
+import typing
+from typing import (
+    Any,
+    Dict,
+    ForwardRef,
+    Generic,
+    Iterable,
+    Protocol,
+    TypedDict,
+    TypeVar,
+    Union,
+    get_args,
+    get_origin,
+    get_type_hints,
+)
+
+from ..common import TypeHint, VarTuple
+from ..feature_requirement import HAS_ANNOTATED, HAS_PY_39, HAS_PY_312, HAS_STD_CLASSES_GENERICS
+from .constants import BUILTIN_ORIGIN_TO_TYPEVARS
+
+TYPED_DICT_MCS = type(types.new_class("_TypedDictSample", (TypedDict,), {}))
+
+
+def strip_alias(type_hint: TypeHint) -> TypeHint:
+    origin = get_origin(type_hint)
+    return type_hint if origin is None else origin
+
+
+def is_subclass_soft(cls, classinfo) -> bool:
+    """Acts like builtin issubclass,
+     but returns False instead of rising TypeError
+    """
+    try:
+        return issubclass(cls, classinfo)
+    except TypeError:
+        return False
+
+
+def has_attrs(obj, attrs: Iterable[str]) -> bool:
+    return all(
+        hasattr(obj, attr_name)
+        for attr_name in attrs
+    )
+
+
+def is_new_type(tp) -> bool:
+    return has_attrs(tp, ['__supertype__', '__name__'])
+
+
+def is_typed_dict_class(tp) -> bool:
+    return isinstance(tp, TYPED_DICT_MCS)
+
+
+NAMED_TUPLE_METHODS = ('_fields', '_field_defaults', '_make', '_replace', '_asdict')
+
+
+def is_named_tuple_class(tp) -> bool:
+    return (
+        is_subclass_soft(tp, tuple)
+        and
+        has_attrs(tp, NAMED_TUPLE_METHODS)
+    )
+
+
+def is_protocol(tp):
+    if not isinstance(tp, type):
+        return False
+
+    return Protocol in tp.__bases__
+
+
+def create_union(args: tuple):
+    return Union[args]
+
+
+if HAS_ANNOTATED:
+    def get_all_type_hints(obj, globalns=None, localns=None):
+        return get_type_hints(obj, globalns, localns, include_extras=True)
+else:
+    get_all_type_hints = get_type_hints
+
+
+def is_parametrized(tp: TypeHint) -> bool:
+    return bool(get_args(tp))
+
+
+def get_type_vars(tp: TypeHint) -> VarTuple[TypeVar]:
+    return getattr(tp, '__parameters__', ())
+
+
+if HAS_PY_312:
+    def is_user_defined_generic(tp: TypeHint) -> bool:
+        # pylint: disable=no-member
+        return (
+            bool(get_type_vars(tp))
+            and (
+                is_subclass_soft(strip_alias(tp), Generic)
+                or isinstance(tp, typing.TypeAliasType)  # type: ignore[attr-defined]
+            )
+        )
+else:
+    def is_user_defined_generic(tp: TypeHint) -> bool:
+        return (
+            bool(get_type_vars(tp))
+            and is_subclass_soft(strip_alias(tp), Generic)
+        )
+
+
+def is_generic(tp: TypeHint) -> bool:
+    """Check if the type could be parameterized"""
+    return (
+        bool(get_type_vars(tp))
+        or (
+            strip_alias(tp) in BUILTIN_ORIGIN_TO_TYPEVARS
+            and tp != type
+            and not is_parametrized(tp)
+            and (
+                bool(HAS_STD_CLASSES_GENERICS) or not isinstance(tp, type)
+            )
+        )
+        or (
+            bool(HAS_ANNOTATED)
+            and get_origin(tp) == typing.Annotated
+            and is_generic(tp.__origin__)
+        )
+    )
+
+
+def is_bare_generic(tp: TypeHint) -> bool:
+    """Check if the type could be parameterized, excluding type aliases (list[T] etc.)"""
+    return (
+        (
+            is_generic(strip_alias(tp))
+            # for 3.8 and List (list is not generic)
+            or is_generic(tp)
+            # at 3.8 list is bare_generic but not generic
+            # (this function only needs to create predicate)
+            or tp in BUILTIN_ORIGIN_TO_TYPEVARS
+        )
+        and not is_parametrized(tp)
+    )
+
+
+def is_generic_class(cls: type) -> bool:
+    """Check if the class represents a generic type.
+    This function is faster than ``.is_generic()``, but it is limited to testing only classes
+    """
+    return (
+        cls in BUILTIN_ORIGIN_TO_TYPEVARS
+        or (
+            issubclass(cls, Generic)  # type: ignore[arg-type]
+            and bool(cls.__parameters__)  # type: ignore[attr-defined]
+        )
+    )
+
+
+def get_type_vars_of_parametrized(tp: TypeHint) -> VarTuple[TypeVar]:
+    try:
+        params = tp.__parameters__
+    except AttributeError:
+        return ()
+
+    if isinstance(tp, type):
+        if HAS_STD_CLASSES_GENERICS and isinstance(tp, types.GenericAlias):
+            return params
+        return ()
+    if get_origin(tp) is not None and get_args(tp) == ():
+        return ()
+    return params
+
+
+if HAS_PY_39:
+    def eval_forward_ref(namespace: Dict[str, Any], forward_ref: ForwardRef):
+        # pylint: disable=protected-access
+        return forward_ref._evaluate(namespace, None, frozenset())
+else:
+    def eval_forward_ref(namespace: Dict[str, Any], forward_ref: ForwardRef):
+        # pylint: disable=protected-access
+        return forward_ref._evaluate(namespace, None)  # type: ignore[call-arg]
diff --git a/src/dishka/_adaptix/type_tools/constants.py b/src/dishka/_adaptix/type_tools/constants.py
new file mode 100644
index 0000000..f05a630
--- /dev/null
+++ b/src/dishka/_adaptix/type_tools/constants.py
@@ -0,0 +1,35 @@
+import collections
+import concurrent.futures
+import queue
+import re
+from os import PathLike
+from typing import Mapping, TypeVar
+
+from ..common import VarTuple
+
+_AnyStrT = TypeVar('_AnyStrT', str, bytes)
+_T1 = TypeVar('_T1')
+_T2 = TypeVar('_T2')
+_T1_co = TypeVar('_T1_co', covariant=True)
+_AnyStr_co = TypeVar("_AnyStr_co", str, bytes, covariant=True)
+
+BUILTIN_ORIGIN_TO_TYPEVARS: Mapping[type, VarTuple[TypeVar]] = {
+    re.Pattern: (_AnyStrT, ),  # type: ignore[dict-item]
+    re.Match: (_AnyStrT, ),  # type: ignore[dict-item]
+    PathLike: (_AnyStr_co, ),  # type: ignore[dict-item]
+    type: (_T1,),  # type: ignore[dict-item]
+    list: (_T1,),  # type: ignore[dict-item]
+    set: (_T1,),  # type: ignore[dict-item]
+    frozenset: (_T1_co, ),  # type: ignore[dict-item]
+    collections.Counter: (_T1,),  # type: ignore[dict-item]
+    collections.deque: (_T1,),  # type: ignore[dict-item]
+    dict: (_T1, _T2),  # type: ignore[dict-item]
+    collections.defaultdict: (_T1, _T2),  # type: ignore[dict-item]
+    collections.OrderedDict: (_T1, _T2),  # type: ignore[dict-item]
+    collections.ChainMap: (_T1, _T2),  # type: ignore[dict-item]
+    queue.Queue: (_T1, ),  # type: ignore[dict-item]
+    queue.PriorityQueue: (_T1, ),  # type: ignore[dict-item]
+    queue.LifoQueue: (_T1, ),  # type: ignore[dict-item]
+    queue.SimpleQueue: (_T1, ),  # type: ignore[dict-item]
+    concurrent.futures.Future: (_T1, ),  # type: ignore[dict-item]
+}
diff --git a/src/dishka/_adaptix/type_tools/generic_resolver.py b/src/dishka/_adaptix/type_tools/generic_resolver.py
new file mode 100644
index 0000000..0a07eb5
--- /dev/null
+++ b/src/dishka/_adaptix/type_tools/generic_resolver.py
@@ -0,0 +1,105 @@
+import typing
+from dataclasses import dataclass, replace
+from itertools import chain
+from typing import Callable, Collection, Dict, Generic, Hashable, Mapping, TypeVar, get_args
+
+from ..common import TypeHint
+from ..feature_requirement import HAS_TV_TUPLE, HAS_UNPACK
+from .basic_utils import get_type_vars, get_type_vars_of_parametrized, is_generic, is_parametrized, strip_alias
+from .implicit_params import fill_implicit_params
+from .normalize_type import normalize_type
+
+M = TypeVar('M')
+K = TypeVar('K', bound=Hashable)
+
+
+@dataclass
+class MembersStorage(Generic[K, M]):
+    members: Mapping[K, TypeHint]
+    overriden: Collection[K]
+    meta: M
+
+
+class GenericResolver(Generic[K, M]):
+    def __init__(self, members_getter: Callable[[TypeHint], MembersStorage[K, M]]):
+        self._raw_members_getter = members_getter
+
+    def get_resolved_members(self, tp: TypeHint) -> MembersStorage[K, M]:
+        if is_parametrized(tp):
+            return self._get_members_of_parametrized_generic(tp)
+        if is_generic(tp):
+            return self._get_members_of_parametrized_generic(fill_implicit_params(tp))
+        return self._get_members_by_parents(tp)
+
+    def _get_members_of_parametrized_generic(self, parametrized_generic) -> MembersStorage[K, M]:
+        origin = strip_alias(parametrized_generic)
+        members_storage = self._get_members_by_parents(origin)
+        type_var_to_actual = self._get_type_var_to_actual(
+            get_type_vars(origin),
+            self._unpack_args(get_args(parametrized_generic)),
+        )
+        return replace(
+            members_storage,
+            members={
+                key: self._parametrize_by_dict(type_var_to_actual, tp)
+                for key, tp in members_storage.members.items()
+            }
+        )
+
+    def _unpack_args(self, args):
+        if HAS_UNPACK and any(strip_alias(arg) == typing.Unpack for arg in args):
+            return tuple(arg.source for arg in normalize_type(tuple[args]).args)
+        return args
+
+    def _get_type_var_to_actual(self, type_vars, args):
+        result = {}
+        idx = 0
+        for tv in type_vars:
+            if HAS_TV_TUPLE and isinstance(tv, typing.TypeVarTuple):
+                tuple_len = len(args) - len(type_vars) + 1
+                result[tv] = args[idx:idx + tuple_len]
+                idx += tuple_len
+            else:
+                result[tv] = (args[idx], )
+                idx += 1
+
+        return result
+
+    def _parametrize_by_dict(self, type_var_to_actual, tp: TypeHint) -> TypeHint:
+        if tp in type_var_to_actual:
+            return type_var_to_actual[tp][0]
+
+        params = get_type_vars_of_parametrized(tp)
+        if not params:
+            return tp
+        return tp[tuple(chain.from_iterable(type_var_to_actual[type_var] for type_var in params))]
+
+    def _get_members_by_parents(self, tp) -> MembersStorage[K, M]:
+        members_storage = self._raw_members_getter(tp)
+        if not any(
+            get_type_vars_of_parametrized(tp) or isinstance(tp, TypeVar)
+            for tp in members_storage.members.values()
+        ):
+            return members_storage
+        if not hasattr(tp, '__orig_bases__'):
+            return members_storage
+
+        bases_members: Dict[K, TypeHint] = {}
+        for base in reversed(tp.__orig_bases__):
+            bases_members.update(self.get_resolved_members(base).members)
+
+        return replace(
+            members_storage,
+            members={
+                key: (
+                    bases_members[key]
+                    if (
+                        key in bases_members
+                        and key not in members_storage.overriden
+                        and (is_generic(value) or isinstance(value, TypeVar))
+                    )
+                    else value
+                )
+                for key, value in members_storage.members.items()
+            },
+        )
diff --git a/src/dishka/_adaptix/type_tools/implicit_params.py b/src/dishka/_adaptix/type_tools/implicit_params.py
new file mode 100644
index 0000000..de133c1
--- /dev/null
+++ b/src/dishka/_adaptix/type_tools/implicit_params.py
@@ -0,0 +1,49 @@
+import sys
+import typing
+from typing import Any, ForwardRef, Tuple, TypeVar
+
+from ..common import TypeHint, VarTuple
+from ..feature_requirement import HAS_PARAM_SPEC, HAS_TV_TUPLE
+from .basic_utils import create_union, eval_forward_ref, is_user_defined_generic, strip_alias
+from .constants import BUILTIN_ORIGIN_TO_TYPEVARS
+
+
+class ImplicitParamsGetter:
+    def _process_limit_element(self, type_var: TypeVar, tp: TypeHint) -> TypeHint:
+        if isinstance(tp, ForwardRef):
+            return eval_forward_ref(vars(sys.modules[type_var.__module__]), tp)
+        return tp
+
+    def _process_type_var(self, type_var) -> TypeHint:
+        if HAS_PARAM_SPEC and isinstance(type_var, typing.ParamSpec):
+            return ...
+        if HAS_TV_TUPLE and isinstance(type_var, typing.TypeVarTuple):
+            return typing.Unpack[Tuple[Any, ...]]
+        if type_var.__constraints__:
+            return create_union(
+                tuple(
+                    self._process_limit_element(type_var, constraint)
+                    for constraint in type_var.__constraints__
+                )
+            )
+        if type_var.__bound__ is None:
+            return Any
+        return self._process_limit_element(type_var, type_var.__bound__)
+
+    def get_implicit_params(self, origin) -> VarTuple[TypeHint]:
+        if is_user_defined_generic(origin):
+            type_vars = origin.__parameters__
+        else:
+            type_vars = BUILTIN_ORIGIN_TO_TYPEVARS.get(origin, ())
+
+        return tuple(
+            self._process_type_var(type_var)
+            for type_var in type_vars
+        )
+
+
+def fill_implicit_params(tp: TypeHint) -> TypeHint:
+    params = ImplicitParamsGetter().get_implicit_params(strip_alias(tp))
+    if params:
+        return tp[params]
+    raise ValueError(f'Can not derive implicit parameters for {tp}')
diff --git a/src/dishka/_adaptix/type_tools/norm_utils.py b/src/dishka/_adaptix/type_tools/norm_utils.py
new file mode 100644
index 0000000..a23e1b6
--- /dev/null
+++ b/src/dishka/_adaptix/type_tools/norm_utils.py
@@ -0,0 +1,40 @@
+import typing
+from dataclasses import InitVar
+from typing import ClassVar, Final, TypeVar
+
+from ..feature_requirement import HAS_ANNOTATED, HAS_TYPED_DICT_REQUIRED
+from .normalize_type import BaseNormType
+
+_TYPE_TAGS = [Final, ClassVar, InitVar]
+
+if HAS_ANNOTATED:
+    _TYPE_TAGS.append(typing.Annotated)
+
+if HAS_TYPED_DICT_REQUIRED:
+    _TYPE_TAGS.extend([typing.Required, typing.NotRequired])
+
+
+def strip_tags(norm: BaseNormType) -> BaseNormType:
+    """Removes type hints that do not represent a type
+    and that only indicates metadata
+    """
+    if norm.origin in _TYPE_TAGS:
+        return strip_tags(norm.args[0])
+    return norm
+
+
+N = TypeVar('N', bound=BaseNormType)
+
+
+def strip_annotated(value: N) -> N:
+    if HAS_ANNOTATED and isinstance(value, BaseNormType) and value.origin == typing.Annotated:
+        return strip_annotated(value)
+    return value
+
+
+def is_class_var(norm: BaseNormType) -> bool:
+    if norm.origin == ClassVar:
+        return True
+    if norm.origin in _TYPE_TAGS:
+        return is_class_var(norm.args[0])
+    return False
diff --git a/src/dishka/_adaptix/type_tools/normalize_type.py b/src/dishka/_adaptix/type_tools/normalize_type.py
new file mode 100644
index 0000000..cc1f090
--- /dev/null
+++ b/src/dishka/_adaptix/type_tools/normalize_type.py
@@ -0,0 +1,876 @@
+# pylint: disable=inconsistent-return-statements,comparison-with-callable
+import dataclasses
+import sys
+import types
+import typing
+from abc import ABC, abstractmethod
+from collections import abc as c_abc, defaultdict
+from copy import copy
+from dataclasses import InitVar, dataclass
+from enum import Enum, EnumMeta
+from functools import lru_cache, partial
+from typing import (
+    Any,
+    Callable,
+    ClassVar,
+    DefaultDict,
+    Dict,
+    Final,
+    ForwardRef,
+    Hashable,
+    Iterable,
+    List,
+    Literal,
+    NewType,
+    NoReturn,
+    Optional,
+    Sequence,
+    Tuple,
+    Type,
+    TypeVar,
+    Union,
+    get_args,
+    overload,
+)
+
+from ..common import TypeHint, VarTuple
+from ..feature_requirement import (
+    HAS_ANNOTATED,
+    HAS_PARAM_SPEC,
+    HAS_PY_310,
+    HAS_PY_311,
+    HAS_SELF_TYPE,
+    HAS_TV_SYNTAX,
+    HAS_TV_TUPLE,
+    HAS_TYPE_ALIAS,
+    HAS_TYPE_GUARD,
+    HAS_TYPE_UNION_OP,
+    HAS_TYPED_DICT_REQUIRED,
+    HAS_UNPACK,
+)
+from .basic_utils import create_union, eval_forward_ref, is_new_type, is_subclass_soft, strip_alias
+from .implicit_params import ImplicitParamsGetter
+
+
+class BaseNormType(Hashable, ABC):
+    @property
+    @abstractmethod
+    def origin(self) -> Any:
+        ...
+
+    @property
+    @abstractmethod
+    def args(self) -> VarTuple[Any]:
+        ...
+
+    @property
+    @abstractmethod
+    def source(self) -> TypeHint:
+        ...
+
+
+T = TypeVar('T')
+
+
+class _BasicNormType(BaseNormType, ABC):
+    __slots__ = ('_source', '_args')
+
+    def __init__(self, args: VarTuple[Any], *, source: TypeHint):
+        self._source = source
+        self._args = args
+
+    @property
+    def args(self) -> VarTuple[Any]:
+        return self._args
+
+    @property
+    def source(self) -> TypeHint:
+        return self._source
+
+    def __hash__(self):
+        return hash((self.origin, self._args))
+
+    def __eq__(self, other):
+        if isinstance(other, _BasicNormType):
+            return (
+                self.origin == other.origin
+                and
+                self._args == other._args
+            )
+        if isinstance(other, BaseNormType):
+            return False
+        return NotImplemented
+
+    def __repr__(self):
+        args_str = f" {list(self.args)}," if self.args else ""
+        return f'<{type(self).__name__}({self.origin},{args_str} source={self._source})>'
+
+
+class _NormType(_BasicNormType):
+    __slots__ = _BasicNormType.__slots__ + ('_source',)
+
+    def __init__(self, origin: TypeHint, args: VarTuple[Any], *, source: TypeHint):
+        self._origin = origin
+        super().__init__(args, source=source)
+
+    @property
+    def origin(self) -> Any:
+        return self._origin
+
+
+class _UnionNormType(_BasicNormType):
+    def __init__(self, args: VarTuple[Any], *, source: TypeHint):
+        super().__init__(self._order_args(args), source=source)
+
+    @property
+    def origin(self) -> Any:
+        return Union
+
+    # ensure stable order of args during one interpreter session
+    def _make_orderable(self, obj: object) -> str:
+        if isinstance(obj, BaseNormType):
+            return f"{obj.origin} {[self._make_orderable(arg) for arg in obj.args]}"
+        return str(obj)
+
+    def _order_args(self, args: VarTuple[BaseNormType]) -> VarTuple[BaseNormType]:
+        args_list = list(args)
+        args_list.sort(key=self._make_orderable)
+        return tuple(args_list)
+
+
+def _type_and_value_iter(args):
+    return [(type(arg), arg) for arg in args]
+
+
+LiteralArg = Union[str, int, bytes, Enum]
+
+
+class _LiteralNormType(_BasicNormType):
+    def __init__(self, args: VarTuple[Any], *, source: TypeHint):
+        super().__init__(self._order_args(args), source=source)
+
+    @property
+    def origin(self) -> Any:
+        return Literal
+
+    # ensure stable order of args during one interpreter session
+    def _make_orderable(self, obj: LiteralArg) -> str:
+        return f"{type(obj)}{obj.name}" if isinstance(obj, Enum) else repr(obj)
+
+    def _order_args(self, args: VarTuple[LiteralArg]) -> VarTuple[LiteralArg]:
+        args_list = list(args)
+        args_list.sort(key=self._make_orderable)
+        return tuple(args_list)
+
+    def __eq__(self, other):
+        if isinstance(other, _LiteralNormType):
+            return _type_and_value_iter(self._args) == _type_and_value_iter(other._args)
+        if isinstance(other, BaseNormType):
+            return False
+        return NotImplemented
+
+    __hash__ = _BasicNormType.__hash__
+
+
+class _AnnotatedNormType(_BasicNormType):
+    @property
+    def origin(self) -> Any:
+        return typing.Annotated
+
+    __slots__ = _BasicNormType.__slots__ + ('_hash',)
+
+    def __init__(self, args: VarTuple[Hashable], *, source: TypeHint):
+        super().__init__(args, source=source)
+        self._hash = self._calc_hash()
+
+    # calculate hash even if one of Annotated metadata is not hashable
+    def _calc_hash(self) -> int:
+        lst = [self.origin]
+        for arg in self._args:
+            try:
+                arg_hash = hash(arg)
+            except TypeError:
+                pass
+            else:
+                lst.append(arg_hash)
+        return hash(tuple(lst))
+
+    def __hash__(self):
+        return self._hash
+
+
+class Variance(Enum):
+    INVARIANT = 0
+    COVARIANT = 1
+    CONTRAVARIANT = 2
+    INFERRED = 3
+
+    def __repr__(self):
+        return f'{type(self).__name__}.{self.name}'
+
+
+@dataclass
+class Bound:
+    value: BaseNormType
+
+
+@dataclass
+class Constraints:
+    value: VarTuple[BaseNormType]
+
+
+TypeVarLimit = Union[Bound, Constraints]  # pylint: disable=invalid-name
+
+
+class NormTV(BaseNormType):
+    __slots__ = ('_var', '_limit', '_variance', '_source')
+
+    def __init__(self, var: Any, limit: TypeVarLimit, *, source: TypeHint):
+        self._var = var
+        self._source = source
+        self._limit = limit
+
+        if var.__covariant__:
+            self._variance = Variance.COVARIANT
+        if var.__contravariant__:
+            self._variance = Variance.CONTRAVARIANT
+        if getattr(var, '__infer_variance__', False):
+            self._variance = Variance.INFERRED
+        self._variance = Variance.INVARIANT
+
+    @property
+    def origin(self) -> Any:
+        return self._var
+
+    @property
+    def args(self) -> Tuple[()]:
+        return ()
+
+    @property
+    def source(self) -> TypeHint:
+        return self._source
+
+    @property
+    def name(self) -> str:
+        return self._var.__name__
+
+    @property
+    def variance(self) -> Variance:
+        return self._variance
+
+    @property
+    def limit(self) -> TypeVarLimit:
+        return self._limit
+
+    def __repr__(self):
+        return f'<{type(self).__name__}({self._var})>'
+
+    def __hash__(self):
+        return hash(self._var)
+
+    def __eq__(self, other):
+        if isinstance(other, NormTV):
+            return self._var == other._var
+        if isinstance(other, BaseNormType):
+            return False
+        return NotImplemented
+
+
+class NormTVTuple(BaseNormType):
+    __slots__ = ('_var', '_source')
+
+    def __init__(self, var: Any, *, source: TypeHint):
+        self._var = var
+        self._source = source
+
+    @property
+    def origin(self) -> Any:
+        return self._var
+
+    @property
+    def args(self) -> Tuple[()]:
+        return ()
+
+    @property
+    def source(self) -> TypeHint:
+        return self._source
+
+    @property
+    def name(self) -> str:
+        return self._var.__name__
+
+    def __repr__(self):
+        return f'<{type(self).__name__}({self._var})>'
+
+    def __hash__(self):
+        return hash(self._var)
+
+    def __eq__(self, other):
+        if isinstance(other, NormTVTuple):
+            return self._var == other._var
+        if isinstance(other, BaseNormType):
+            return False
+        return NotImplemented
+
+
+class NormParamSpecMarker(BaseNormType, ABC):
+    __slots__ = ('_param_spec', '_source')
+
+    def __init__(self, param_spec: Any, *, source: TypeHint):
+        self._param_spec = param_spec
+        self._source = source
+
+    @property
+    def param_spec(self) -> NormTV:
+        return self._param_spec
+
+    @property
+    def args(self) -> Tuple[()]:
+        return ()
+
+    @property
+    def source(self) -> TypeHint:
+        return self._source
+
+    def __hash__(self):
+        return hash((self.origin, self.param_spec))
+
+    def __eq__(self, other):
+        if isinstance(other, NormParamSpecMarker):
+            return self.origin == other.origin and self._param_spec == other._param_spec
+        if isinstance(other, BaseNormType):
+            return False
+        return NotImplemented
+
+
+class _NormParamSpecArgs(NormParamSpecMarker):
+    @property
+    def origin(self) -> Any:
+        return typing.ParamSpecArgs
+
+
+class _NormParamSpecKwargs(NormParamSpecMarker):
+    @property
+    def origin(self) -> Any:
+        return typing.ParamSpecKwargs
+
+
+AnyNormTypeVarLike = Union[NormTV, NormTVTuple]
+
+
+class NormTypeAlias(BaseNormType):
+    __slots__ = ('_type_alias', '_args', '_norm_type_vars')
+
+    def __init__(self, type_alias, args: VarTuple[BaseNormType], type_vars: VarTuple[AnyNormTypeVarLike]):
+        self._type_alias = type_alias
+        self._args = args
+        self._type_vars = type_vars
+
+    @property
+    def origin(self) -> Any:
+        return self._type_alias
+
+    @property
+    def args(self) -> VarTuple[BaseNormType]:
+        return self._args
+
+    @property
+    def source(self) -> TypeHint:
+        return self._type_alias
+
+    @property
+    def value(self):
+        return self._type_alias.__value__
+
+    @property
+    def module(self):
+        return self._type_alias.__module__
+
+    @property
+    def type_params(self) -> VarTuple[AnyNormTypeVarLike]:
+        return self._type_vars
+
+    def __eq__(self, other):
+        if isinstance(other, NormTypeAlias):
+            return self._type_alias == other._type_alias and self._args == other._args
+        if isinstance(other, BaseNormType):
+            return False
+        return NotImplemented
+
+    def __hash__(self):
+        return hash(self._type_alias)
+
+
+_PARAM_SPEC_MARKER_TYPES = (typing.ParamSpecArgs, typing.ParamSpecKwargs) if HAS_PARAM_SPEC else ()
+
+
+def make_norm_type(  # noqa: CCR001
+    origin: TypeHint,
+    args: VarTuple[Hashable],
+    *,
+    source: TypeHint
+) -> BaseNormType:
+    if origin == Union:
+        if not all(isinstance(arg, BaseNormType) for arg in args):
+            raise TypeError
+        return _UnionNormType(args, source=source)
+    if origin == Literal:
+        if not all(type(arg) in [int, bool, str, bytes] or isinstance(type(arg), EnumMeta) for arg in args):
+            raise TypeError
+        return _LiteralNormType(args, source=source)
+    if HAS_ANNOTATED and origin == typing.Annotated:
+        return _AnnotatedNormType(args, source=source)
+    if isinstance(origin, TypeVar):
+        raise TypeError
+    if HAS_PARAM_SPEC and (
+        isinstance(origin, _PARAM_SPEC_MARKER_TYPES) or isinstance(source, _PARAM_SPEC_MARKER_TYPES)
+    ):
+        raise TypeError
+    return _NormType(origin, args, source=source)
+
+
+NoneType = type(None)
+ANY_NT = _NormType(Any, (), source=Any)
+
+
+def _create_norm_union(args: VarTuple[BaseNormType]) -> BaseNormType:
+    return _UnionNormType(args, source=create_union(tuple(a.source for a in args)))
+
+
+def _dedup(inp: Iterable[T]) -> Iterable[T]:
+    in_set = set()
+    result = []
+    for item in inp:
+        if item not in in_set:
+            result.append(item)
+            in_set.add(item)
+    return result
+
+
+def _create_norm_literal(args: Iterable):
+    dedup_args = tuple(_dedup(args))
+    return _LiteralNormType(
+        dedup_args,
+        source=Literal[dedup_args],
+    )
+
+
+def _replace_source(norm: BaseNormType, *, source: TypeHint) -> BaseNormType:
+    norm_copy = copy(norm)
+    norm_copy._source = source  # type: ignore[attr-defined]
+    return norm_copy
+
+
+def _replace_source_with_union(norm: BaseNormType, sources: list) -> BaseNormType:
+    return _replace_source(
+        norm=norm,
+        source=create_union(tuple(sources)),
+    )
+
+
+NormAspect = Callable[['TypeNormalizer', Any, Any, tuple], Optional[BaseNormType]]
+
+
+class AspectStorage(List[str]):
+    @overload
+    def add(self, *, condition: object = True) -> Callable[[NormAspect], NormAspect]:
+        ...
+
+    @overload
+    def add(self, func: NormAspect) -> NormAspect:
+        ...
+
+    def add(self, func: Optional[NormAspect] = None, *, condition: object = True) -> Any:
+        if func is None:
+            return partial(self.add, condition=condition)
+
+        if condition:
+            self.append(func.__name__)
+        return func
+
+    def copy(self) -> 'AspectStorage':
+        return type(self)(super().copy())
+
+
+class NotSubscribedError(ValueError):
+    pass
+
+
+N = TypeVar('N', bound=BaseNormType)
+TN = TypeVar('TN', bound='TypeNormalizer')
+
+
+class TypeNormalizer:
+    def __init__(self, implicit_params_getter: ImplicitParamsGetter):
+        self.implicit_params_getter = implicit_params_getter
+        self._namespace: Optional[Dict[str, Any]] = None
+
+    def _with_namespace(self: TN, namespace: Dict[str, Any]) -> TN:
+        # pylint: disable=protected-access
+        self_copy = copy(self)
+        self_copy._namespace = namespace
+        return self_copy
+
+    def _with_module_namespace(self: TN, module_name: str) -> TN:
+        try:
+            module = sys.modules[module_name]
+        except KeyError:
+            return self
+        return self._with_namespace(vars(module))
+
+    @overload
+    def normalize(self, tp: TypeVar) -> NormTV:
+        ...
+
+    @overload
+    def normalize(self, tp: TypeHint) -> BaseNormType:
+        ...
+
+    def normalize(self, tp: TypeHint) -> BaseNormType:
+        origin = strip_alias(tp)
+        args = get_args(tp)
+
+        result = self._norm_forward_ref(tp)
+        if result is not None:
+            return result
+
+        for attr_name in self._aspect_storage:
+            result = getattr(self, attr_name)(tp, origin, args)
+            if result is not None:
+                return result
+
+        raise RuntimeError
+
+    def _norm_forward_ref(self, tp):
+        if self._namespace is not None:
+            if isinstance(tp, str):
+                return _replace_source(
+                    self.normalize(ForwardRef(tp)),
+                    source=tp,
+                )
+            if isinstance(tp, ForwardRef):
+                return _replace_source(
+                    self.normalize(eval_forward_ref(self._namespace, tp)),
+                    source=tp
+                )
+        elif isinstance(tp, (str, ForwardRef)):
+            raise ValueError(f'Can not normalize value {tp!r}, there are no namespace to evaluate types')
+
+    _aspect_storage = AspectStorage()
+
+    def _norm_iter(self, tps: Iterable[Any]) -> VarTuple[BaseNormType]:
+        return tuple(self.normalize(tp) for tp in tps)
+
+    MUST_SUBSCRIBED_ORIGINS = [
+        ClassVar, Final, Literal,
+        Union, Optional, InitVar,
+    ]
+    if HAS_ANNOTATED:
+        MUST_SUBSCRIBED_ORIGINS.append(typing.Annotated)
+    if HAS_TYPE_GUARD:
+        MUST_SUBSCRIBED_ORIGINS.append(typing.TypeGuard)
+    if HAS_TYPED_DICT_REQUIRED:
+        MUST_SUBSCRIBED_ORIGINS.extend([typing.Required, typing.NotRequired])
+
+    @_aspect_storage.add
+    def _check_bad_input(self, tp, origin, args):
+        if tp in self.MUST_SUBSCRIBED_ORIGINS:
+            raise NotSubscribedError(f"{tp} must be subscribed")
+
+        if tp in (NewType, TypeVar):
+            raise ValueError(f'{origin} must be instantiating')
+
+    @_aspect_storage.add
+    def _norm_none(self, tp, origin, args):
+        if origin is None or origin is NoneType:
+            return _NormType(None, (), source=tp)
+
+    @_aspect_storage.add(condition=HAS_ANNOTATED)
+    def _norm_annotated(self, tp, origin, args):
+        if origin == typing.Annotated:
+            return _AnnotatedNormType(
+                (self.normalize(args[0]), *args[1:]),
+                source=tp
+            )
+
+    def _get_bound(self, type_var) -> Bound:
+        return (
+            Bound(ANY_NT)
+            if (type_var.__bound__ is None or type_var.__bound__ is NoneType) else
+            Bound(self.normalize(type_var.__bound__))
+        )
+
+    @_aspect_storage.add
+    def _norm_type_var(self, tp, origin, args):
+        # pylint: disable=protected-access
+        if isinstance(origin, TypeVar):
+            namespaced = self._with_module_namespace(origin.__module__)
+            limit = (
+                Constraints(
+                    tuple(
+                        namespaced._dedup_union_args(
+                            namespaced._norm_iter(origin.__constraints__)
+                        )
+                    )
+                )
+                if origin.__constraints__ else
+                namespaced._get_bound(origin)
+            )
+            return NormTV(var=origin, limit=limit, source=tp)
+
+    @_aspect_storage.add(condition=HAS_TV_TUPLE)
+    def _norm_type_var_tuple(self, tp, origin, args):
+        if isinstance(origin, typing.TypeVarTuple):
+            return NormTVTuple(var=origin, source=tp)
+
+    @_aspect_storage.add(condition=HAS_PARAM_SPEC)
+    def _norm_param_spec(self, tp, origin, args):
+        if isinstance(tp, typing.ParamSpecArgs):
+            return _NormParamSpecArgs(param_spec=self.normalize(origin), source=tp)
+
+        if isinstance(tp, typing.ParamSpecKwargs):
+            return _NormParamSpecKwargs(param_spec=self.normalize(origin), source=tp)
+
+        if isinstance(origin, typing.ParamSpec):
+            namespaced = self._with_module_namespace(origin.__module__)
+            return NormTV(
+                var=origin,
+                limit=namespaced._get_bound(origin),  # pylint: disable=protected-access
+                source=tp,
+            )
+
+    @_aspect_storage.add(condition=HAS_TV_SYNTAX)
+    def _norm_type_alias_type(self, tp, origin, args):
+        if isinstance(origin, typing.TypeAliasType):  # pylint: disable=no-member
+            return NormTypeAlias(
+                origin,
+                self._norm_iter(args),
+                self._norm_iter(tp.__type_params__),
+            )
+
+    @_aspect_storage.add
+    def _norm_init_var(self, tp, origin, args):
+        if isinstance(origin, InitVar):
+            # this origin is InitVar[T]
+            return _NormType(
+                InitVar,
+                (self.normalize(origin.type),),
+                source=tp
+            )
+
+    @_aspect_storage.add
+    def _norm_new_type(self, tp, origin, args):
+        if is_new_type(tp):
+            return _NormType(tp, (), source=tp)
+
+    @_aspect_storage.add
+    def _norm_tuple(self, tp, origin, args):
+        if origin == tuple:
+            if tp in (tuple, Tuple):  # not subscribed values
+                return _NormType(
+                    tuple,
+                    (ANY_NT, ...),
+                    source=tp
+                )
+
+            # >>> Tuple[()].__args__ == ((),)
+            # >>> tuple[()].__args__ == ()
+            if not args or args == ((),):
+                return _NormType(tuple, (), source=tp)
+
+            if args[-1] is Ellipsis:
+                return _NormType(
+                    tuple, (*self._norm_iter(args[:-1]), ...),
+                    source=tp,
+                )
+
+            norm_args = self._norm_iter(args)
+            if HAS_UNPACK:
+                norm_args = self._unpack_tuple_elements(norm_args)
+            return _NormType(tuple, norm_args, source=tp)
+
+    def _unpack_tuple_elements(self, args: VarTuple[BaseNormType]) -> VarTuple[BaseNormType]:
+        # it is necessary to unpack the variable-length tuple as well
+        if len(args) == 1 and args[0].origin == typing.Unpack:
+            inner_tp = args[0].args[0]
+            if inner_tp.origin == tuple:
+                return inner_tp.args
+
+        return self._unpack_generic_elements(args)
+
+    def _unpack_generic_elements(self, args: VarTuple[Any]) -> VarTuple[BaseNormType]:
+        result = []
+        for arg in args:
+            if isinstance(arg, BaseNormType) and arg.origin == typing.Unpack and self._is_fixed_size_tuple(arg.args[0]):
+                result.extend(arg.args[0].args)
+            else:
+                result.append(arg)
+        return tuple(result)
+
+    def _is_fixed_size_tuple(self, tp: BaseNormType) -> bool:
+        return tp.origin == tuple and (not tp.args or tp.args[-1] is not Ellipsis)
+
+    @_aspect_storage.add
+    def _norm_callable(self, tp, origin, args):
+        if origin == c_abc.Callable:
+            if not args:
+                return _NormType(
+                    c_abc.Callable, (..., ANY_NT), source=tp
+                )
+
+            if args[0] is Ellipsis:
+                call_args = ...
+            elif isinstance(args[0], list):
+                call_args = self._norm_iter(args[0])
+                if HAS_TV_TUPLE:
+                    call_args = self._unpack_generic_elements(call_args)
+            else:
+                call_args = normalize_type(args[0])
+            return _NormType(
+                c_abc.Callable, (call_args, self.normalize(args[-1])), source=tp
+            )
+
+    @_aspect_storage.add
+    def _norm_literal(self, tp, origin, args):
+        if origin == Literal:
+            if args == (None,):  # Literal[None] converted to None
+                return _NormType(None, (), source=tp)
+
+            if None in args:
+                args_without_none = list(args)
+                args_without_none.remove(None)
+
+                return _UnionNormType(
+                    (
+                        _NormType(None, (), source=Literal[None]),
+                        _create_norm_literal(args_without_none),
+                    ),
+                    source=tp
+                )
+
+            return _LiteralNormType(args, source=tp)
+
+    def _unfold_union_args(self, norm_args: Iterable[N]) -> Iterable[N]:
+        result: List[N] = []
+        for norm in norm_args:
+            if norm.origin == Union:
+                result.extend(norm.args)
+            else:
+                result.append(norm)
+        return result
+
+    def _dedup_union_args(self, args: Iterable[BaseNormType]) -> Iterable[BaseNormType]:
+        args_to_sources: DefaultDict[BaseNormType, List[Any]] = defaultdict(list)
+
+        for arg in args:
+            args_to_sources[arg].append(arg.source)
+
+        return [
+            _replace_source_with_union(arg, sources)
+            if len(sources) != 1 and isinstance(arg, _NormType)
+            else arg
+            for arg, sources in args_to_sources.items()
+        ]
+
+    def _merge_literals(self, args: Iterable[N]) -> Sequence[N]:
+        result = []
+        lit_args: List[N] = []
+        for norm in args:
+            if norm.origin == Literal:
+                lit_args.extend(norm.args)
+            else:
+                result.append(norm)
+
+        if lit_args:
+            result.append(_create_norm_literal(lit_args))
+        return result
+
+    _UNION_ORIGINS: List[Any] = [Union]
+    if HAS_TYPE_UNION_OP:
+        _UNION_ORIGINS.append(types.UnionType)
+
+    @_aspect_storage.add
+    def _norm_union(self, tp, origin, args):
+        if origin in self._UNION_ORIGINS:
+            norm_args = self._norm_iter(args)
+            unfolded_n_args = self._unfold_union_args(norm_args)
+            unique_n_args = self._dedup_union_args(unfolded_n_args)
+            merged_n_args = self._merge_literals(unique_n_args)
+
+            if len(merged_n_args) == 1:
+                arg = merged_n_args[0]
+                return make_norm_type(origin=arg.origin, args=arg.args, source=tp)
+            return _UnionNormType(tuple(merged_n_args), source=tp)
+
+    @_aspect_storage.add
+    def _norm_type(self, tp, origin, args):
+        if is_subclass_soft(origin, type) and args:
+            norm = self.normalize(args[0])
+
+            if norm.origin == Union:
+                return _UnionNormType(
+                    tuple(
+                        _NormType(type, (arg,), source=Type[arg.source])
+                        for arg in norm.args
+                    ),
+                    source=tp
+                )
+
+    ALLOWED_ZERO_PARAMS_ORIGINS = {Any, NoReturn}
+    if HAS_TYPE_ALIAS:
+        ALLOWED_ZERO_PARAMS_ORIGINS.add(typing.TypeAlias)
+    if HAS_PY_310:
+        ALLOWED_ZERO_PARAMS_ORIGINS.add(dataclasses.KW_ONLY)
+    if HAS_PY_311:
+        ALLOWED_ZERO_PARAMS_ORIGINS.add(typing.Never)
+    if HAS_SELF_TYPE:
+        ALLOWED_ZERO_PARAMS_ORIGINS.add(typing.Self)
+    if HAS_PY_311:
+        ALLOWED_ZERO_PARAMS_ORIGINS.add(typing.LiteralString)
+
+    def _norm_generic_arg(self, arg):
+        if arg is Ellipsis:
+            return Ellipsis
+        if isinstance(arg, tuple):
+            return self._norm_iter(arg)
+        return self.normalize(arg)
+
+    def _norm_implicit_param(self, param):
+        if param is Ellipsis:
+            return Ellipsis
+        return self.normalize(param)
+
+    @_aspect_storage.add
+    def _norm_other(self, tp, origin, args):
+        if args:
+            norm_args = tuple(self._norm_generic_arg(el) for el in args)
+            if HAS_TV_TUPLE:
+                norm_args = self._unpack_generic_elements(norm_args)
+            return _NormType(origin, norm_args, source=tp)
+
+        params = self.implicit_params_getter.get_implicit_params(origin)
+        if not (
+            params
+            or isinstance(origin, type)
+            or origin in self.ALLOWED_ZERO_PARAMS_ORIGINS
+        ):
+            raise ValueError(f'Can not normalize value {tp!r}')
+
+        return _NormType(
+            origin,
+            tuple(self._norm_implicit_param(param) for param in params),
+            source=tp,
+        )
+
+
+_STD_NORMALIZER = TypeNormalizer(ImplicitParamsGetter())
+_cached_normalize = lru_cache(maxsize=128)(_STD_NORMALIZER.normalize)
+
+
+def normalize_type(tp: TypeHint) -> BaseNormType:
+    try:
+        hash(tp)
+    except TypeError:
+        return _STD_NORMALIZER.normalize(tp)
+
+    return _cached_normalize(tp)
diff --git a/src/dishka/dependency_source.py b/src/dishka/dependency_source.py
index 162e69c..92b6d99 100644
--- a/src/dishka/dependency_source.py
+++ b/src/dishka/dependency_source.py
@@ -4,6 +4,7 @@ from inspect import (
     isasyncgenfunction,
     isclass,
     iscoroutinefunction,
+    isfunction,
     isgeneratorfunction,
 )
 from typing import (
@@ -18,6 +19,11 @@ from typing import (
     overload,
 )
 
+from ._adaptix.type_tools.basic_utils import get_all_type_hints
+from ._adaptix.type_tools.generic_resolver import (
+    GenericResolver,
+    MembersStorage,
+)
 from .scope import BaseScope
 
 
@@ -73,24 +79,41 @@ class Factory:
         )
 
 
+def _get_init_members(tp) -> MembersStorage[str, None]:
+    type_hints = get_all_type_hints(tp.__init__)
+    if "__init__" in tp.__dict__:
+        overriden = frozenset(type_hints)
+    else:
+        overriden = {}
+
+    return MembersStorage(
+        meta=None,
+        members=type_hints,
+        overriden=overriden,
+    )
+
+
 def make_factory(
         provides: Any,
         scope: Optional[BaseScope],
         source: Callable,
 ) -> Factory:
-    if isclass(source):
-        hints = get_type_hints(source.__init__, include_extras=True)
+    if isclass(source) or get_origin(source):
+        # we need to fix concrete generics and normal classes as well
+        # as classes can be children of concrete generics
+        res = GenericResolver(_get_init_members)
+        hints = dict(res.get_resolved_members(source).members)
         hints.pop("return", None)
         possible_dependency = source
         is_to_bind = False
-    else:
+    elif isfunction(source):
         hints = get_type_hints(source, include_extras=True)
         possible_dependency = hints.pop("return", None)
         is_to_bind = True
+    else:
+        raise TypeError(f"Cannot use {type(source)} as a factory")
 
-    if isclass(source):
-        provider_type = FactoryType.FACTORY
-    elif isasyncgenfunction(source):
+    if isasyncgenfunction(source):
         provider_type = FactoryType.ASYNC_GENERATOR
         if get_origin(possible_dependency) is AsyncIterable:
             possible_dependency = get_args(possible_dependency)[0]
diff --git a/src/dishka/integrations/aiogram.py b/src/dishka/integrations/aiogram.py
index 19e24ae..fad5868 100644
--- a/src/dishka/integrations/aiogram.py
+++ b/src/dishka/integrations/aiogram.py
@@ -3,6 +3,7 @@ __all__ = [
     "inject",
     "setup_dishka",
 ]
+
 from inspect import Parameter
 from typing import Container, Sequence
 
@@ -12,12 +13,10 @@ from aiogram.types import TelegramObject
 from dishka import Provider, make_async_container
 from .base import Depends, wrap_injection
 
-CONTAINER_NAME = "dishka_container"
-
 
 def inject(func):
     additional_params = [Parameter(
-        name=CONTAINER_NAME,
+        name="dishka_container",
         annotation=Container,
         kind=Parameter.KEYWORD_ONLY,
     )]
@@ -25,7 +24,7 @@ def inject(func):
     return wrap_injection(
         func=func,
         remove_depends=True,
-        container_getter=lambda _, p: p[CONTAINER_NAME],
+        container_getter=lambda _, p: p["dishka_container"],
         additional_params=additional_params,
         is_async=True,
     )
@@ -39,8 +38,8 @@ class ContainerMiddleware(BaseMiddleware):
     async def __call__(
             self, handler, event, data,
     ):
-        async with self.container({TelegramObject: event}) as sub_container:
-            data[CONTAINER_NAME] = sub_container
+        async with self.container({TelegramObject: event}) as subcontainer:
+            data["dishka_container"] = subcontainer
             return await handler(event, data)
 
     async def startup(self):
diff --git a/src/dishka/integrations/telebot.py b/src/dishka/integrations/telebot.py
index d5b6678..cd41f13 100644
--- a/src/dishka/integrations/telebot.py
+++ b/src/dishka/integrations/telebot.py
@@ -13,11 +13,10 @@ from telebot import BaseMiddleware, TeleBot
 from dishka import Container, Provider, make_container
 from .base import Depends, wrap_injection
 
-CONTAINER_NAME = "dishka_container"
 
 def inject(func):
     additional_params = [Parameter(
-        name=CONTAINER_NAME,
+        name="dishka_container",
         annotation=Container,
         kind=Parameter.KEYWORD_ONLY,
     )]
@@ -25,7 +24,7 @@ def inject(func):
     return wrap_injection(
         func=func,
         remove_depends=True,
-        container_getter=lambda _, p: p[CONTAINER_NAME],
+        container_getter=lambda _, p: p["dishka_container"],
         additional_params=additional_params,
         is_async=False,
     )
@@ -40,11 +39,11 @@ class ContainerMiddleware(BaseMiddleware):
 
     def pre_process(self, message, data):
         dishka_container_wrapper = self.container({type(message): message})
-        data[CONTAINER_NAME + "_wrapper"] = dishka_container_wrapper
-        data[CONTAINER_NAME] = dishka_container_wrapper.__enter__()
+        data["dishka_container_wrapper"] = dishka_container_wrapper
+        data["dishka_container"] = dishka_container_wrapper.__enter__()
 
     def post_process(self, message, data, exception):
-        data[CONTAINER_NAME + "_wrapper"].__exit__(None, None, None)
+        data["dishka_container_wrapper"].__exit__(None, None, None)
 
 
 def setup_dishka(providers: Sequence[Provider], bot: TeleBot) -> Container:


Test patch:
diff --git a/src/dishka/py.typed b/src/dishka/py.typed
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/unit/container/test_generic.py b/tests/unit/container/test_generic.py
new file mode 100644
index 0000000..738fbab
--- /dev/null
+++ b/tests/unit/container/test_generic.py
@@ -0,0 +1,67 @@
+from typing import Generic, TypeVar
+
+import pytest
+
+from dishka import Provider, Scope, make_container, provide
+
+T = TypeVar("T")
+U = TypeVar("U")
+
+
+class Base(Generic[T]):
+    def __init__(self, y: T):
+        self.y = y
+
+
+class ReplaceInit(Base[str], Generic[T]):
+    def __init__(self, x: T):
+        super().__init__("hello")
+        self.x = x
+
+
+class A(Generic[T]):
+    def __init__(self, x: T):
+        self.x = x
+
+
+class B(A[U], Generic[U]):
+    pass
+
+
+@pytest.mark.parametrize(
+    "cls", [A, B, ReplaceInit],
+)
+def test_concrete_generic(cls):
+    class MyProvider(Provider):
+        scope = Scope.APP
+
+        @provide
+        def get_int(self) -> int:
+            return 42
+
+        a = provide(cls[int])
+
+    with make_container(MyProvider()) as container:
+        a = container.get(cls[int])
+        assert isinstance(a, cls)
+        assert a.x == 42
+
+
+class C(A[int]):
+    pass
+
+
+def test_concrete_child():
+    class MyProvider(Provider):
+        scope = Scope.APP
+
+        @provide
+        def get_int(self) -> int:
+            return 42
+
+        a = provide(C)
+
+    with make_container(MyProvider()) as container:
+        a = container.get(C)
+        assert isinstance(a, C)
+        assert a.x == 42