Skip to content

Commit

Permalink
comparer: express comparisons in terms of the comparer
Browse files Browse the repository at this point in the history
  • Loading branch information
delfick committed Oct 8, 2023
1 parent f977852 commit 4533cf4
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 219 deletions.
29 changes: 22 additions & 7 deletions strcs/disassemble/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
fields_from_class,
fields_from_dataclasses,
)
from ._instance_check import InstanceCheck, create_checkable
from ._instance_check import InstanceCheck, InstanceCheckMeta, create_checkable
from ._score import Score

if tp.TYPE_CHECKING:
Expand Down Expand Up @@ -184,23 +184,38 @@ def __eq__(self, o: object) -> tp.TypeGuard["Type"]:
if o is Type.Missing:
return True

if isinstance(o, InstanceCheck) and hasattr(o, "Meta"):
if issubclass(type(o), InstanceCheckMeta) and hasattr(o, "Meta"):
o = o.Meta.disassembled

if isinstance(o, Type):
if issubclass(type(o), Type) and hasattr(o, "original"):
o = o.original

if (
o == self.original
or (self.is_annotated and o == self.extracted)
or (self.optional and o is None)
or (self.mro.all_vars and o == self.origin)
or (self.is_union and o in self.nonoptional_union_types)
):
return True

if type(o) in union_types:
return len(set(tp.get_args(o)) - set(self.relevant_types)) == 0
else:
return o in self.relevant_types
for part in self.relevant_types:
disassembled = self.disassemble.typed(object, part)
if o == disassembled.original:
return True
elif disassembled.is_annotated and o == disassembled.extracted:
return True
elif disassembled.optional and o is None:
return True
elif disassembled.is_union and o in disassembled.nonoptional_union_types:
return True
elif disassembled.mro.all_vars and o == disassembled.origin:
return True

return False

def for_display(self) -> str:
"""
Expand Down Expand Up @@ -533,7 +548,7 @@ def is_type_for(self, instance: object) -> tp.TypeGuard[T]:
Whether this type represents the type for some object. Uses the
``isinstance`` check on the :class:`strcs.InstanceCheck` for this object.
"""
return isinstance(instance, self.checkable)
return self.cache.comparer.isinstance(instance, self)

def is_equivalent_type_for(self, value: object) -> tp.TypeGuard[T]:
"""
Expand Down Expand Up @@ -638,11 +653,11 @@ def func_from(
not considered matches. In the second pass they are.
"""
for want, func in sorted(options, key=lambda pair: pair[0], reverse=True):
if self.checkable.matches(want.checkable):
if self.cache.comparer.matches(self, want):
return func

for want, func in sorted(options, key=lambda pair: pair[0], reverse=True):
if self.checkable.matches(want.checkable, subclasses=True):
if self.cache.comparer.matches(self, want, subclasses=True):
return func

return None
Expand Down
3 changes: 3 additions & 0 deletions strcs/disassemble/_cache.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import typing as tp
from collections.abc import MutableMapping

from ._comparer import Comparer

if tp.TYPE_CHECKING:
from ._base import Disassembler, Type

Expand Down Expand Up @@ -76,6 +78,7 @@ class TypeCache(MutableMapping[object, "Type"]):
def __init__(self) -> None:
self.cache: dict[tuple[type, object], "Type"] = {}
self.disassemble = _TypeCacheDisassembler(self)
self.comparer = Comparer(self)

def key(self, o: object) -> tuple[type, object]:
return (type(o), o)
Expand Down
235 changes: 39 additions & 196 deletions strcs/disassemble/_instance_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,8 @@ class InstanceCheck(abc.ABC):
* ``type``
* ``hash``
The ``checkable.matches`` method will check that another type is equivalent depending on the options provided.
* ``subclass=True``: If a subclass can be counted as matching
* ``allow_missing_typevars``: If an unfilled generic counts as matching a filled generic
* For unions, it ill match another union if there is a complete overlap in items between both unions
For classes or unions that are only one type with None
------------------------------------------------------
For objects that aren't unions or an optional type
--------------------------------------------------
For these, the ``check_against`` will be the return of ``disassembled.origin``
Expand Down Expand Up @@ -99,7 +93,7 @@ class Meta:
optional: bool
"True if the value being wrapped is a ``typing.Optional`` or Union with ``None``"

union_types: tuple[type["InstanceCheck"]] | None
union_types: tuple["Type", ...] | None
"A tuple of the types in the union if it's a union, otherwise ``None``"

disassembled: "Type"
Expand All @@ -111,186 +105,77 @@ class Meta:
without_annotation: object
"The original object given to :class:`strcs.Type` without a wrapping ``typing.Annotation``"

@classmethod
def matches(
cls,
other: type["InstanceCheck"],
subclasses: bool = False,
allow_missing_typevars: bool = True,
) -> bool:
"""
Used to determine if this is equivalent to another object.
"""
raise NotImplementedError()


def create_checkable(disassembled: "Type") -> type[InstanceCheck]:
class Meta(InstanceCheck.Meta):
original = disassembled.original
extracted = disassembled.extracted
extracted = disassembled.without_annotation
optional = disassembled.optional
without_optional = disassembled.without_optional
without_annotation = disassembled.without_annotation

Meta.disassembled = disassembled
check_against: object | None

if tp.get_origin(Meta.extracted) in union_types:
check_against = tuple(
disassembled.disassemble(a).checkable for a in tp.get_args(Meta.extracted)
)
check_against = tuple(disassembled.disassemble(a) for a in tp.get_args(Meta.extracted))

reprstr = " | ".join(repr(c) for c in check_against)

Meta.typ = Meta.extracted
Meta.union_types = tp.cast(tuple[type[InstanceCheck]], check_against)
Checker = _checker_union(disassembled, check_against, Meta)
Meta.union_types = check_against
else:
check_against_single: type | None = disassembled.origin
if Meta.extracted is None:
check_against_single = None
check_against = None if Meta.extracted is None else disassembled.origin

reprstr = repr(check_against)

Meta.typ = disassembled.origin
Meta.union_types = None
Checker = _checker_single(disassembled, check_against_single, Meta)

if hasattr(Meta.extracted, "__args__"):
Checker.__args__ = Meta.extracted.__args__ # type: ignore
if hasattr(Meta.extracted, "__origin__"):
Checker.__origin__ = Meta.extracted.__origin__ # type: ignore
if hasattr(Meta.extracted, "__parameters__"):
Checker.__parameters__ = Meta.extracted.__parameters__ # type: ignore
if hasattr(Meta.extracted, "__annotations__"):
Checker.__annotations__ = Meta.extracted.__annotations__ # type: ignore
if hasattr(Checker.Meta.typ, "__attrs_attrs__"):
Checker.__attrs_attrs__ = Checker.Meta.typ.__attrs_attrs__ # type:ignore
if hasattr(Checker.Meta.typ, "__dataclass_fields__"):
Checker.__dataclass_fields__ = Checker.Meta.typ.__dataclass_fields__ # type:ignore

return Checker


def _checker_union(
disassembled: "Type",
check_against: tp.Sequence[type],
M: type[InstanceCheck.Meta],
) -> type[InstanceCheck]:

reprstr = " | ".join(repr(c) for c in check_against)

class CheckerMeta(InstanceCheckMeta):
def __repr__(self) -> str:
return reprstr

def __instancecheck__(self, obj: object) -> bool:
return (obj is None and disassembled.optional) or isinstance(obj, tuple(check_against))

def __eq__(self, o: object) -> bool:
return any(o == ch for ch in check_against)

def __hash__(self) -> int:
if type(M.extracted) is type:
return hash(M.extracted)
else:
return id(disassembled)

@property # type:ignore
def __class__(self) -> type:
return type(M.extracted)

@classmethod
def __subclasscheck__(cls, C: type) -> bool:
if C == CombinedMeta:
return True

if hasattr(C, "Meta") and issubclass(C.Meta, InstanceCheck.Meta):
if isinstance(C.Meta.typ, type):
C = C.Meta.typ
return issubclass(C, tuple(check_against))

class CombinedMeta(CheckerMeta, abc.ABCMeta):
pass

class Checker(InstanceCheck, metaclass=CombinedMeta):
def __new__(mcls, *args, **kwargs):
raise ValueError(f"Cannot instantiate a union type: {check_against}")

@classmethod
def matches(
cls,
other: type[InstanceCheck],
subclasses: bool = False,
allow_missing_typevars=False,
) -> bool:
if cls.Meta.union_types is None or other.Meta.union_types is None:
return False

if subclasses:
# I want it so that everything in cls is a subclass of other
if not all(issubclass(typ, other) for typ in cls.Meta.union_types):
return False

# And for all types in other to have a matching subclass in cls
if not all(
any(issubclass(cls_typ, other_typ) for cls_typ in cls.Meta.union_types)
for other_typ in other.Meta.union_types
):
return False

return True
else:
for typ in cls.Meta.union_types:
found = False
for other_typ in other.Meta.union_types:
if typ.matches(other_typ):
found = True
break
if not found:
return False

if len(cls.Meta.union_types) == len(other.Meta.union_types):
return True

for typ in other.Meta.union_types:
found = False
for cls_typ in cls.Meta.union_types:
if other_typ.matches(cls_typ):
found = True
break
if not found:
return False
Checker = _create_checker(disassembled, check_against, Meta, reprstr)

return True
typ = disassembled.origin
extracted = Meta.extracted

Meta = M
if hasattr(extracted, "__args__"):
Checker.__args__ = extracted.__args__ # type: ignore
if hasattr(extracted, "__origin__"):
Checker.__origin__ = extracted.__origin__ # type: ignore
if hasattr(extracted, "__parameters__"):
Checker.__parameters__ = extracted.__parameters__ # type: ignore
if hasattr(extracted, "__annotations__"):
Checker.__annotations__ = extracted.__annotations__ # type: ignore
if hasattr(typ, "__attrs_attrs__"):
Checker.__attrs_attrs__ = typ.__attrs_attrs__ # type:ignore
if hasattr(typ, "__dataclass_fields__"):
Checker.__dataclass_fields__ = typ.__dataclass_fields__ # type:ignore

return Checker


def _checker_single(
def _create_checker(
disassembled: "Type",
check_against: object | None,
check_against: object,
M: type[InstanceCheck.Meta],
reprstr: str,
) -> type[InstanceCheck]:
from ._base import Type
comparer = disassembled.cache.comparer

class CheckerMeta(InstanceCheckMeta):
def __repr__(self) -> str:
return repr(check_against)
return reprstr

def __instancecheck__(self, obj: object) -> bool:
if check_against is None:
return obj is None

return (obj is None and disassembled.optional) or isinstance(
obj, tp.cast(type, check_against)
)
return comparer.isinstance(obj, M.original)

def __eq__(self, o: object) -> bool:
return o == check_against
return o == disassembled or o is type(M.extracted)

def __hash__(self) -> int:
if type(M.extracted) is type:
return hash(M.extracted)
else:
return id(disassembled)
return id(self)

@property # type:ignore
def __class__(self) -> type:
Expand All @@ -301,58 +186,16 @@ def __subclasscheck__(cls, C: type) -> bool:
if C == CombinedMeta:
return True

if not isinstance(check_against, type):
return False

if hasattr(C, "Meta") and issubclass(C.Meta, InstanceCheck.Meta):
if isinstance(C.Meta.typ, type):
C = C.Meta.typ

if not issubclass(C, check_against):
return False

want = disassembled.disassemble(C)
for w, g in zip(want.mro.all_vars, disassembled.mro.all_vars):
if isinstance(w, Type) and isinstance(g, Type):
if not issubclass(w.checkable, g.checkable):
return False

return True
return comparer.issubclass(C, M.original)

class CombinedMeta(CheckerMeta, abc.ABCMeta):
pass

class Checker(InstanceCheck, metaclass=CombinedMeta):
def __new__(mcls, *args, **kwargs):
return check_against(*args, **kwargs)

@classmethod
def matches(
cls,
other: type[InstanceCheck],
subclasses: bool = False,
allow_missing_typevars=False,
) -> bool:
if subclasses:
if not issubclass(cls, other):
return False

for ctv, otv in zip(
cls.Meta.disassembled.mro.all_vars,
other.Meta.disassembled.mro.all_vars,
):
if isinstance(ctv, Type) and isinstance(otv, Type):
if not ctv.checkable.matches(otv.checkable, subclasses=True):
return False
elif otv is Type.Missing and not allow_missing_typevars:
return False

return True
else:
return (
cls.Meta.typ == other.Meta.typ
and cls.Meta.disassembled.mro.all_vars == other.Meta.disassembled.mro.all_vars
)
if callable(check_against):
return check_against(*args, **kwargs)
raise ValueError(f"Cannot instantiate this type: {check_against}")

Meta = M

Expand Down
Loading

0 comments on commit 4533cf4

Please sign in to comment.