Skip to content

Commit

Permalink
Merge pull request #51 from reagento/feature/generics
Browse files Browse the repository at this point in the history
support concrete generics
  • Loading branch information
Tishka17 authored Feb 17, 2024
2 parents c799222 + e5eae0b commit a053949
Show file tree
Hide file tree
Showing 13 changed files with 1,611 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
exclude_lines =
pragma: not covered
@overload
[run]
omit =
src/dishka/_adaptix/**
1 change: 1 addition & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
line-length = 79
src = ["src"]
include = ["src/**.py", "tests/**.py"]
exclude = ["src/dishka/_adaptix/**"]

lint.select = [
"E",
Expand Down
22 changes: 22 additions & 0 deletions src/dishka/_adaptix/common.py
Original file line number Diff line number Diff line change
@@ -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)
178 changes: 178 additions & 0 deletions src/dishka/_adaptix/feature_requirement.py
Original file line number Diff line number Diff line change
@@ -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')
26 changes: 26 additions & 0 deletions src/dishka/_adaptix/type_tools/__init__.py
Original file line number Diff line number Diff line change
@@ -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,
)
Loading

0 comments on commit a053949

Please sign in to comment.