in chz/tiepin.py [0:0]
def is_subtype_instance(inst: typing.Any, typ: TypeForm) -> bool:
if typ is typing.Any or typ is typing_extensions.Any:
return True
if typ is None and inst is None:
return True
if isinstance(typ, typing.TypeVar):
if typ.__constraints__:
# types must match exactly
return any(
type(inst) is getattr(c, "__origin__", c) and is_subtype_instance(inst, c)
for c in typ.__constraints__
)
if typ.__bound__:
return is_subtype_instance(inst, typ.__bound__)
return True
if isinstance(typ, typing.NewType):
return isinstance(inst, typ.__supertype__)
origin: typing.Any
args: typing.Any
if sys.version_info >= (3, 10) and isinstance(typ, types.UnionType):
origin = typing.Union
else:
origin = getattr(typ, "__origin__", typ)
args = getattr(typ, "__args__", ())
del typ
if origin is typing.Union:
return any(is_subtype_instance(inst, t) for t in args)
if origin is typing.Literal or origin is typing_extensions.Literal:
return inst in args
if origin is typing.LiteralString:
return isinstance(inst, str)
if is_typed_dict(origin):
if not isinstance(inst, dict):
return False
for k, v in typing_extensions.get_type_hints(origin).items():
if k in inst:
if not is_subtype_instance(inst[k], v):
return False
elif k in origin.__required_keys__:
return False
return True
# Pydantic implements generics in a special way. Just delegate validation to Pydantic.
# Note that all pydantic models have __pydantic_generic_metadata__, even non-generic ones.
if hasattr(origin, "__pydantic_generic_metadata__"):
from pydantic import ValidationError
try:
origin.model_validate(inst)
return True
except ValidationError:
return False
if typing_extensions.is_protocol(origin):
if getattr(origin, "_is_runtime_protocol", False):
return isinstance(inst, origin)
if origin in type(inst).__mro__:
return True
annotations = typing_extensions.get_type_hints(origin)
for attr in sorted(typing_extensions.get_protocol_members(origin)):
if not hasattr(inst, attr):
return False
if attr in annotations:
if not is_subtype_instance(getattr(inst, attr), annotations[attr]):
return False
elif callable(getattr(origin, attr)):
if attr == "__call__" and isinstance(inst, (type, types.FunctionType)):
# inst will have a better inspect.signature than inst.__call__
inst_attr = inst
else:
inst_attr = getattr(inst, attr)
if not callable(inst_attr):
return False
try:
signature = _SignatureOf(getattr(origin, attr), strip_self=True)
except ValueError:
continue
if not is_subtype_instance(inst_attr, signature):
return False
else:
raise AssertionError(f"Unexpected protocol member {attr} for {origin}")
return True
if isinstance(origin, _SignatureOf):
try:
inst_sig = _SignatureOf(inst)
except ValueError:
return True
return is_subtype(inst_sig, origin)
# We're done handling special forms, now just need to handle things like generics
if not isinstance(origin, type):
# TODO: handle other special forms before exit on this branch
return False
if not isinstance(inst, origin):
# PEP 484 duck type compatibility
if origin is complex and isinstance(inst, (int, float)):
return True
if origin is float and isinstance(inst, int):
return True
if origin is bytes and isinstance(inst, (bytearray, memoryview)):
# TODO: maybe remove bytearray and memoryview ducktyping based on PEP 688
return True
if inst in typing_Never:
return True
if issubclass(type(inst), typing_extensions.Any) or (
sys.version_info >= (3, 11) and issubclass(type(inst), typing.Any)
):
return True
return False
assert isinstance(inst, origin)
if not args:
return True
# TODO: there's some confusion when checking issubclass against a generic collections.abc
# base class, since you don't actually know whether the generic args of typ / origin correspond
# to the generic args of the base class. So if we detect a user defined generic (i.e. based
# on presence of Generic in the mro), we just fall back and don't assume we know the semantics
# of what the generic args are.
if issubclass(origin, collections.abc.Mapping) and typing.Generic not in origin.__mro__:
key_type, value_type = args
return all(
is_subtype_instance(key, key_type) and is_subtype_instance(value, value_type)
for key, value in inst.items()
)
if origin is tuple:
if len(args) == 2 and args[1] is ...:
return all(is_subtype_instance(i, args[0]) for i in inst)
if len(inst) != len(args):
return False
return all(is_subtype_instance(i, t) for i, t in zip(inst, args))
if issubclass(origin, collections.abc.Iterable) and typing.Generic not in origin.__mro__:
(item_type,) = args
return all(is_subtype_instance(item, item_type) for item in inst)
if origin is type:
(type_type,) = args
return issubclass(inst, type_type)
if origin is collections.abc.Callable:
try:
inst_sig = inspect.signature(inst)
except ValueError:
return True
*params, ret = args
if params != [...]:
try:
bound = inst_sig.bind(*params)
except TypeError:
return False
for param, callable_param_type in bound.arguments.items():
param = inst_sig.parameters[param]
param_annot = maybe_eval_in_context(param.annotation, inst)
# ooh, contravariance
if param.kind is param.VAR_POSITIONAL:
if any(not is_subtype(cpt, param_annot) for cpt in callable_param_type):
return False
elif not is_subtype(callable_param_type, param_annot):
return False
if inst_sig.return_annotation is not inst_sig.empty:
ret_annot = maybe_eval_in_context(inst_sig.return_annotation, inst)
# inspect.signature(Cls) will have Cls.__init__, which is annotated as -> None
if not (isinstance(inst, type) and ret_annot is None and is_subtype(inst, ret)):
if ret_annot is None:
ret_annot = type(None)
elif ret_annot in typing_Never:
ret_annot = ret
if ret_annot != ret and not is_subtype(ret_annot, ret):
return False
return True
# We don't really know how to handle user defined generics
if hasattr(inst, "__orig_class__"):
# If we have an __orig_class__ and the origins match, check the args (assuming that they
# are invariant, although maybe covariant is a better guess?)
if inst.__orig_class__.__origin__ is origin:
return inst.__orig_class__.__args__ == args
# Otherwise, fail open
return True