Typechecker stdlib module :shipit:
This commit is contained in:
parent
6f8b5a4e96
commit
5316cff2a2
6 changed files with 736 additions and 1709 deletions
4
examples/typechecker.it
Normal file
4
examples/typechecker.it
Normal file
|
@ -0,0 +1,4 @@
|
|||
import typecheck as tc
|
||||
tc.checktype('hello', int | str) |> print
|
||||
tc.checktype(0xdeadbeef, int | str) |> print
|
||||
tc.checktype(['hello', 0xdeadbeef], int | str) |> print
|
|
@ -43,7 +43,7 @@ class lazy_typegetter:
|
|||
|
||||
|
||||
|
||||
def translate(file: io.StringIO, debug: int = 0):
|
||||
def translate(file: io.StringIO, debug: int = 0, bcp: bool = False):
|
||||
def infix(name: str):
|
||||
yield tokenize.OP, ">>"
|
||||
yield tokenize.NAME, name
|
||||
|
@ -147,7 +147,7 @@ def translate(file: io.StringIO, debug: int = 0):
|
|||
yield type,name
|
||||
dprint(f'---END DEBOUT---', 4)
|
||||
|
||||
def transpile(input_path: Path, verbosity: int, minify: bool) -> None:
|
||||
def transpile(input_path: Path, verbosity: int, minify: bool, bcp: bool) -> None:
|
||||
dir = Path('dist')
|
||||
if input_path.is_dir():
|
||||
for i in input_path.glob('*'):
|
||||
|
@ -176,17 +176,19 @@ app = typer.Typer()
|
|||
|
||||
verbosity_arg = typing.Annotated[int, typer.Option('--verbosity', '-v')]
|
||||
minify_arg = typing.Annotated[bool, typer.Option('--minify', '-m')]
|
||||
bcp_arg = typing.Annotated[bool, typer.Option('--build-custom-prefix', '-B')]
|
||||
|
||||
|
||||
@app.command('t')
|
||||
@app.command('ts')
|
||||
@app.command('transpile')
|
||||
def transpile_cmd(input_path: pathlib.Path, verbosity: verbosity_arg = 0, minify: minify_arg = False) -> None:
|
||||
transpile(input_path, verbosity, minify)
|
||||
def transpile_cmd(input_path: pathlib.Path, verbosity: verbosity_arg = 0, minify: minify_arg = False, bcp: bcp_arg = False) -> None:
|
||||
transpile(input_path, verbosity, minify, bcp)
|
||||
@app.command('r')
|
||||
@app.command('run')
|
||||
def run_cmd(input_path: pathlib.Path, verbosity: verbosity_arg = 0, minify: minify_arg = False) -> None:
|
||||
def run_cmd(input_path: pathlib.Path, verbosity: verbosity_arg = 0, minify: minify_arg = False, bcp: bcp_arg = False) -> None:
|
||||
input_path = Path(input_path)
|
||||
transpile(input_path, verbosity, minify)
|
||||
transpile(input_path, verbosity, minify, bcp)
|
||||
if input_path.is_dir():
|
||||
os.system(f'{sys.executable} -m dist')
|
||||
Path('dist').rmtree()
|
||||
|
|
|
@ -65,6 +65,7 @@ def _INTERNAL_add_fakeimport(name: str, code: str): # TODO: make this use sys.me
|
|||
|
||||
_INTERNAL_add_fakeimport('sentinels', std'sentinels.py')
|
||||
_INTERNAL_add_fakeimport('ipathlib', std'ipathlib.py')
|
||||
_INTERNAL_add_fakeimport('typecheck', std'typecheck.py')
|
||||
_INTERNAL_lazymerge = _INTERNAL_Token(lambda lhs, rhs: _INTERNAL_LazyIterable(lhs, rhs))
|
||||
|
||||
_INTERNAL_lpipe = _INTERNAL_Token(lambda lhs, rhs: rhs(lhs))
|
||||
|
|
714
std/typecheck.py
Normal file
714
std/typecheck.py
Normal file
|
@ -0,0 +1,714 @@
|
|||
from typing import (
|
||||
_GenericAlias,
|
||||
_TypedDictMeta,
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
ForwardRef,
|
||||
FrozenSet,
|
||||
List,
|
||||
Literal,
|
||||
Mapping,
|
||||
MutableMapping,
|
||||
MutableSequence,
|
||||
NamedTuple,
|
||||
Never,
|
||||
NewType,
|
||||
NoReturn,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeAliasType,
|
||||
TypeGuard,
|
||||
TypeVar,
|
||||
Union,
|
||||
_eval_type as eval_type,
|
||||
_type_repr as type_repr,
|
||||
cast,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_type_hints,
|
||||
overload
|
||||
)
|
||||
from collections.abc import Callable as CCallable, Mapping as CMapping, MutableMapping as CMutableMapping, MutableSequence as CMutableSequence, Sequence as CSequence
|
||||
from inspect import Parameter
|
||||
from types import UnionType, ModuleType, GenericAlias
|
||||
import functools, math, inspect, sys, importlib, re, builtins
|
||||
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_F = TypeVar("_F")
|
||||
_SimpleTypeVar = TypeVar("_SimpleTypeVar")
|
||||
_SimpleTypeVarCo = TypeVar("_SimpleTypeVarCo", covariant=True)
|
||||
|
||||
|
||||
_MISSING = object()
|
||||
|
||||
_IMPORTABLE_TYPE_EXPRESSION_RE = re.compile(r"^((?:[a-zA-Z0-9_]+\.)+)(.*)$")
|
||||
_UNIMPORTABLE_TYPE_EXPRESSION_RE = re.compile(r"^[a-zA-Z0-9_]+(\[.*\])?$")
|
||||
_BUILTINS_MODULE: ModuleType = builtins
|
||||
_EXTRA_ADVISE_IF_MOD_IS_BUILTINS = (
|
||||
" Try altering the type argument to be a string "
|
||||
"reference (surrounded with quotes) instead, "
|
||||
"if not already done."
|
||||
)
|
||||
|
||||
class UnresolvedForwardRefError(TypeError):...
|
||||
|
||||
class UnresolvableTypeError(TypeError):...
|
||||
|
||||
class ValidationError(TypeError):...
|
||||
|
||||
class TypeNotSupportedError(TypeError):...
|
||||
|
||||
class _TrycastOptions(NamedTuple):
|
||||
strict: bool
|
||||
eval: bool
|
||||
funcname: str
|
||||
|
||||
class _LazyStr(str):
|
||||
def __init__(self, value_func: Callable[[], str], /) -> None:
|
||||
self._value_func = value_func
|
||||
self._value = None # type: Optional[str]
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self._value is None:
|
||||
self._value = self._value_func()
|
||||
return self._value
|
||||
|
||||
def _substitute(tp: object, substitutions: Dict[object, object]) -> object:
|
||||
if isinstance(tp, GenericAlias): # ex: tuple[T1, T2]
|
||||
return GenericAlias( # type: ignore[reportCallIssue] # pyright
|
||||
tp.__origin__, tuple([_substitute(a, substitutions) for a in tp.__args__])
|
||||
)
|
||||
if isinstance(tp, TypeVar): # type: ignore[wrong-arg-types] # pytype
|
||||
return substitutions.get(tp, tp)
|
||||
return tp
|
||||
|
||||
def _inspect_signature(value):
|
||||
return inspect.signature(
|
||||
value,
|
||||
# Don't auto-unwrap decorated functions
|
||||
follow_wrapped=False,
|
||||
# Don't need annotation information
|
||||
eval_str=False,
|
||||
)
|
||||
|
||||
def _is_typed_dict(tp: object) -> bool:
|
||||
return isinstance(tp, _TypedDictMeta)
|
||||
|
||||
|
||||
|
||||
def _is_newtype(tp: object) -> bool:
|
||||
return isinstance(tp, NewType)
|
||||
|
||||
def _is_simple_typevar(T: object, covariant: bool = False) -> bool:
|
||||
return (
|
||||
isinstance(T, TypeVar) # type: ignore[wrong-arg-types] # pytype
|
||||
and T.__constraints__ == () # type: ignore[attribute-error] # pytype
|
||||
and T.__covariant__ == covariant # type: ignore[attribute-error] # pytype
|
||||
and T.__contravariant__ is False # type: ignore[attribute-error] # pytype
|
||||
and T.__constraints__ == () # type: ignore[attribute-error] # pytype
|
||||
)
|
||||
|
||||
def _checkcast_listlike(
|
||||
tp: object,
|
||||
value: object,
|
||||
listlike_type: Type,
|
||||
options: _TrycastOptions,
|
||||
*,
|
||||
covariant_t: bool = False,
|
||||
t_ellipsis: bool = False,
|
||||
) -> "Optional[ValidationError]":
|
||||
if isinstance(value, listlike_type):
|
||||
T_ = get_args(tp)
|
||||
|
||||
if len(T_) == 0: # Python 3.9+
|
||||
(T,) = (_SimpleTypeVarCo if covariant_t else _SimpleTypeVar,)
|
||||
|
||||
else:
|
||||
if t_ellipsis:
|
||||
if len(T_) == 2 and T_[1] is Ellipsis:
|
||||
(T, _) = T_
|
||||
else:
|
||||
return ValidationError(tp, value)
|
||||
else:
|
||||
(T,) = T_
|
||||
|
||||
if _is_simple_typevar(T, covariant=covariant_t):
|
||||
pass
|
||||
else:
|
||||
for i, x in enumerate(value): # type: ignore[attribute-error] # pytype
|
||||
e = _checkcast_inner(T, x, options)
|
||||
if e is not None:
|
||||
return ValidationError(
|
||||
tp,
|
||||
value,
|
||||
_causes=[e._with_prefix(_LazyStr(lambda: f"At index {i}"))],
|
||||
)
|
||||
|
||||
return None
|
||||
else:
|
||||
return ValidationError(tp, value)
|
||||
|
||||
|
||||
def _checkcast_dictlike(
|
||||
tp: object,
|
||||
value: object,
|
||||
dictlike_type: Type,
|
||||
options: _TrycastOptions,
|
||||
*,
|
||||
covariant_v: bool = False,
|
||||
) -> "Optional[ValidationError]":
|
||||
if isinstance(value, dictlike_type):
|
||||
K_V = get_args(tp)
|
||||
|
||||
if len(K_V) == 0: # Python 3.9+
|
||||
(K, V) = (
|
||||
_SimpleTypeVar,
|
||||
_SimpleTypeVarCo if covariant_v else _SimpleTypeVar,
|
||||
)
|
||||
else:
|
||||
(K, V) = K_V
|
||||
|
||||
if _is_simple_typevar(K) and _is_simple_typevar(V, covariant=covariant_v):
|
||||
pass
|
||||
else:
|
||||
for k, v in value.items(): # type: ignore[attribute-error] # pytype
|
||||
e = _checkcast_inner(K, k, options)
|
||||
if e is not None:
|
||||
return ValidationError(
|
||||
tp,
|
||||
value,
|
||||
_causes=[e._with_prefix(_LazyStr(lambda: f"Key {k!r}"))],
|
||||
)
|
||||
e = _checkcast_inner(V, v, options)
|
||||
if e is not None:
|
||||
return ValidationError(
|
||||
tp,
|
||||
value,
|
||||
_causes=[e._with_prefix(_LazyStr(lambda: f"At key {k!r}"))],
|
||||
)
|
||||
return None
|
||||
else:
|
||||
return ValidationError(tp, value)
|
||||
|
||||
def _type_check(arg: object, msg: str):
|
||||
"""Returns the argument if it appears to be a type.
|
||||
Raises TypeError if the argument is a known non-type.
|
||||
|
||||
As a special case, accepts None and returns type(None) instead.
|
||||
Also wraps strings into ForwardRef instances.
|
||||
"""
|
||||
arg = _type_convert(arg, module=None)
|
||||
# Recognize *common* non-types. (This check is not exhaustive.)
|
||||
if isinstance(arg, (dict, list, int, tuple)):
|
||||
raise TypeError(f"{msg} Got {arg!r:.100}.")
|
||||
return arg
|
||||
|
||||
# Python 3.10's typing._type_convert()
|
||||
def _type_convert(arg, module=None):
|
||||
"""For converting None to type(None), and strings to ForwardRef."""
|
||||
if arg is None:
|
||||
return type(None)
|
||||
if isinstance(arg, str):
|
||||
return ForwardRef(arg, module=module)
|
||||
return arg
|
||||
|
||||
|
||||
@overload
|
||||
def checktype(
|
||||
value: object, tp: str, /, *, eval: Literal[False]
|
||||
) -> NoReturn: ... # pragma: no cover
|
||||
|
||||
|
||||
@overload
|
||||
def checktype(
|
||||
value: object, tp: str, /, *, eval: bool = True
|
||||
) -> bool: ... # pragma: no cover
|
||||
|
||||
|
||||
@overload
|
||||
def checktype(value: object, tp: Type[_T], /, *, eval: bool = True) -> TypeGuard[_T]: # type: ignore[invalid-annotation] # pytype
|
||||
... # pragma: no cover
|
||||
|
||||
|
||||
@overload
|
||||
def checktype(
|
||||
value: object, tp: object, /, *, eval: bool = True
|
||||
) -> bool: ... # pragma: no cover
|
||||
|
||||
|
||||
def checktype(value, tp, /, *, eval=True):
|
||||
"""
|
||||
Returns whether `value` is in the shape of `tp`
|
||||
(as accepted by a Python typechecker conforming to PEP 484 "Type Hints").
|
||||
|
||||
This method logically performs an operation similar to:
|
||||
|
||||
return isinstance(value, tp)
|
||||
|
||||
except that it supports many more types than `isinstance`, including:
|
||||
* List[T]
|
||||
* Dict[K, V]
|
||||
* Optional[T]
|
||||
* Union[T1, T2, ...]
|
||||
* Literal[...]
|
||||
* T extends TypedDict
|
||||
|
||||
See trycast.trycast(..., strict=True) for information about parameters,
|
||||
raised exceptions, and other details.
|
||||
"""
|
||||
e = _checkcast_outer(
|
||||
tp, value, _TrycastOptions(strict=True, eval=eval, funcname="isassignable")
|
||||
)
|
||||
result = e is None
|
||||
if isinstance(tp, type):
|
||||
return cast( # type: ignore[invalid-annotation] # pytype
|
||||
TypeGuard[_T], # type: ignore[not-indexable] # pytype
|
||||
result,
|
||||
)
|
||||
else:
|
||||
return result # type: ignore[bad-return-type] # pytype
|
||||
|
||||
def _checkcast_outer(
|
||||
tp: object, value: object, options: _TrycastOptions
|
||||
) -> "Optional[ValidationError]":
|
||||
if isinstance(tp, str):
|
||||
if options.eval: # == options.eval (for pytype)
|
||||
tp = eval_type_str(tp) # does use eval()
|
||||
else:
|
||||
raise UnresolvableTypeError(
|
||||
f"Could not resolve type {tp!r}: "
|
||||
f"Type appears to be a string reference "
|
||||
f"and {options.funcname}() was called with eval=False, "
|
||||
f"disabling eval of string type references."
|
||||
)
|
||||
else:
|
||||
try:
|
||||
# TODO: Eliminate format operation done by f-string
|
||||
# from the hot path of _checkcast_outer()
|
||||
tp = _type_check( # type: ignore[16] # pyre
|
||||
tp,
|
||||
f"{options.funcname}() requires a type as its first argument.",
|
||||
)
|
||||
except TypeError:
|
||||
if isinstance(tp, tuple) and len(tp) >= 1 and isinstance(tp[0], type):
|
||||
raise TypeError(
|
||||
f"{options.funcname} does not support checking against a tuple of types. "
|
||||
"Try checking against a Union[T1, T2, ...] instead."
|
||||
)
|
||||
else:
|
||||
raise
|
||||
try:
|
||||
return _checkcast_inner(tp, value, options) # type: ignore[bad-return-type] # pytype
|
||||
except UnresolvedForwardRefError:
|
||||
if options.eval:
|
||||
advise = (
|
||||
"Try altering the first type argument to be a string "
|
||||
"reference (surrounded with quotes) instead."
|
||||
)
|
||||
else:
|
||||
advise = (
|
||||
f"{options.funcname}() cannot resolve string type references "
|
||||
"because it was called with eval=False."
|
||||
)
|
||||
raise UnresolvedForwardRefError(
|
||||
f"{options.funcname} does not support checking against type form {tp!r} "
|
||||
"which contains a string-based forward reference. "
|
||||
f"{advise}"
|
||||
)
|
||||
|
||||
def _checkcast_inner(
|
||||
tp: object, value: object, options: _TrycastOptions
|
||||
) -> "Optional[ValidationError]":
|
||||
"""
|
||||
Raises:
|
||||
* TypeNotSupportedError
|
||||
* UnresolvedForwardRefError
|
||||
"""
|
||||
if tp is int:
|
||||
# Also accept bools as valid int values
|
||||
if isinstance(value, int):
|
||||
return None
|
||||
else:
|
||||
return ValidationError(tp, value)
|
||||
|
||||
if tp is float:
|
||||
# Also accept ints and bools as valid float values
|
||||
if isinstance(value, float) or isinstance(value, int):
|
||||
return None
|
||||
else:
|
||||
return ValidationError(tp, value)
|
||||
|
||||
if tp is complex:
|
||||
# Also accept floats, ints, and bools as valid complex values
|
||||
if (
|
||||
isinstance(value, complex)
|
||||
or isinstance(value, float)
|
||||
or isinstance(value, int)
|
||||
):
|
||||
return None
|
||||
else:
|
||||
return ValidationError(tp, value)
|
||||
|
||||
type_origin = get_origin(tp)
|
||||
|
||||
if type_origin is list or type_origin is List: # List, List[T]
|
||||
return _checkcast_listlike(tp, value, list, options)
|
||||
|
||||
if type_origin is set or type_origin is Set: # Set, Set[T]
|
||||
return _checkcast_listlike(tp, value, set, options)
|
||||
|
||||
if type_origin is frozenset or type_origin is FrozenSet: # FrozenSet, FrozenSet[T]
|
||||
return _checkcast_listlike(tp, value, frozenset, options, covariant_t=True)
|
||||
|
||||
if type_origin is tuple or type_origin is Tuple:
|
||||
if isinstance(value, tuple):
|
||||
type_args = get_args(tp)
|
||||
|
||||
if len(type_args) == 0 or (
|
||||
len(type_args) == 2 and type_args[1] is Ellipsis
|
||||
): # Tuple, Tuple[T, ...]
|
||||
|
||||
return _checkcast_listlike(
|
||||
tp,
|
||||
value,
|
||||
tuple,
|
||||
options,
|
||||
covariant_t=True,
|
||||
t_ellipsis=True,
|
||||
)
|
||||
else: # Tuple[Ts]
|
||||
if len(value) != len(type_args):
|
||||
return ValidationError(tp, value)
|
||||
|
||||
for i, T, t in zip(range(len(type_args)), type_args, value):
|
||||
e = _checkcast_inner(T, t, options)
|
||||
if e is not None:
|
||||
return ValidationError(
|
||||
tp,
|
||||
value,
|
||||
_causes=[e._with_prefix(_LazyStr(lambda: f"At index {i}"))],
|
||||
)
|
||||
|
||||
return None
|
||||
else:
|
||||
return ValidationError(tp, value)
|
||||
|
||||
if type_origin is Sequence or type_origin is CSequence: # Sequence, Sequence[T]
|
||||
return _checkcast_listlike(tp, value, CSequence, options, covariant_t=True)
|
||||
|
||||
if (
|
||||
type_origin is MutableSequence or type_origin is CMutableSequence
|
||||
): # MutableSequence, MutableSequence[T]
|
||||
return _checkcast_listlike(tp, value, CMutableSequence, options)
|
||||
|
||||
if type_origin is dict or type_origin is Dict: # Dict, Dict[K, V]
|
||||
return _checkcast_dictlike(tp, value, dict, options)
|
||||
|
||||
if type_origin is Mapping or type_origin is CMapping: # Mapping, Mapping[K, V]
|
||||
return _checkcast_dictlike(tp, value, CMapping, options, covariant_v=True)
|
||||
|
||||
if (
|
||||
type_origin is MutableMapping or type_origin is CMutableMapping
|
||||
): # MutableMapping, MutableMapping[K, V]
|
||||
return _checkcast_dictlike(tp, value, CMutableMapping, options)
|
||||
|
||||
if (
|
||||
type_origin is Union or type_origin is UnionType
|
||||
): # Union[T1, T2, ...], Optional[T]
|
||||
causes = []
|
||||
for T in get_args(tp):
|
||||
e = _checkcast_inner(T, value, options)
|
||||
if e is not None:
|
||||
causes.append(e)
|
||||
else:
|
||||
return None
|
||||
return ValidationError(tp, value, causes)
|
||||
|
||||
if type_origin is Literal: # Literal[...]
|
||||
for literal in get_args(tp):
|
||||
if value == literal:
|
||||
return None
|
||||
return ValidationError(tp, value)
|
||||
|
||||
if type_origin is CCallable:
|
||||
callable_args = get_args(tp)
|
||||
if callable_args == ():
|
||||
# Callable
|
||||
if callable(value):
|
||||
return None
|
||||
else:
|
||||
return ValidationError(tp, value)
|
||||
else:
|
||||
assert len(callable_args) == 2
|
||||
(param_types, return_type) = callable_args
|
||||
|
||||
if return_type is not Any:
|
||||
# Callable[..., T]
|
||||
raise TypeNotSupportedError(
|
||||
f"{options.funcname} cannot reliably determine whether value is "
|
||||
f"a {type_repr(tp)} because "
|
||||
f"callables at runtime do not always have a "
|
||||
f"declared return type. "
|
||||
f"Consider using {options.funcname}(Callable, value) instead."
|
||||
)
|
||||
|
||||
if param_types is Ellipsis:
|
||||
# Callable[..., Any]
|
||||
return _checkcast_inner(Callable, value, options)
|
||||
|
||||
assert isinstance(param_types, list)
|
||||
for param_type in param_types:
|
||||
if param_type is not Any:
|
||||
raise TypeNotSupportedError(
|
||||
f"{options.funcname} cannot reliably determine whether value is "
|
||||
f"a {type_repr(tp)} because "
|
||||
f"callables at runtime do not always have "
|
||||
f"declared parameter types. "
|
||||
f"Consider using {options.funcname}("
|
||||
f"Callable[{','.join('Any' * len(param_types))}, Any], value) "
|
||||
f"instead."
|
||||
)
|
||||
|
||||
# Callable[[Any * N], Any]
|
||||
if callable(value):
|
||||
try:
|
||||
sig = _inspect_signature(value)
|
||||
except TypeError:
|
||||
# Not a callable
|
||||
return ValidationError(tp, value)
|
||||
except ValueError as f:
|
||||
# Unable to introspect signature for value.
|
||||
# It might be a built-in function that lacks signature support.
|
||||
# Assume conservatively that value does NOT match the requested type.
|
||||
e = ValidationError(tp, value)
|
||||
e.__cause__ = f
|
||||
return e
|
||||
else:
|
||||
sig_min_param_count = 0 # type: float
|
||||
sig_max_param_count = 0 # type: float
|
||||
for expected_param in sig.parameters.values():
|
||||
if (
|
||||
expected_param.kind == Parameter.POSITIONAL_ONLY
|
||||
or expected_param.kind == Parameter.POSITIONAL_OR_KEYWORD
|
||||
):
|
||||
if expected_param.default is Parameter.empty:
|
||||
sig_min_param_count += 1
|
||||
sig_max_param_count += 1
|
||||
elif expected_param.kind == Parameter.VAR_POSITIONAL:
|
||||
sig_max_param_count = math.inf
|
||||
|
||||
if sig_min_param_count <= len(param_types) <= sig_max_param_count:
|
||||
return None
|
||||
else:
|
||||
return ValidationError(tp, value)
|
||||
else:
|
||||
return ValidationError(tp, value)
|
||||
|
||||
if isinstance(type_origin, TypeAliasType): # type: ignore[16] # pyre
|
||||
if len(type_origin.__type_params__) > 0:
|
||||
substitutions = dict(
|
||||
zip(
|
||||
type_origin.__type_params__,
|
||||
get_args(tp) + ((Any,) * len(type_origin.__type_params__)),
|
||||
)
|
||||
) # type: Dict[object, object]
|
||||
new_tp = _substitute(tp.__value__, substitutions) # type: ignore[attr-defined] # mypy
|
||||
else:
|
||||
new_tp = tp.__value__ # type: ignore[attr-defined] # mypy
|
||||
return _checkcast_inner(new_tp, value, options) # type: ignore[16] # pyre
|
||||
|
||||
if isinstance(tp, _GenericAlias): # type: ignore[16] # pyre
|
||||
raise TypeNotSupportedError(
|
||||
f"{options.funcname} does not know how to recognize generic type "
|
||||
f"{type_repr(type_origin)}."
|
||||
)
|
||||
|
||||
if _is_typed_dict(tp): # T extends TypedDict
|
||||
if isinstance(value, Mapping):
|
||||
if options.eval:
|
||||
resolved_annotations = get_type_hints( # does use eval()
|
||||
tp # type: ignore[arg-type] # mypy
|
||||
) # resolve ForwardRefs in tp.__annotations__
|
||||
else:
|
||||
resolved_annotations = tp.__annotations__ # type: ignore[attribute-error] # pytype
|
||||
|
||||
try:
|
||||
# {typing in Python 3.9+, typing_extensions}.TypedDict
|
||||
required_keys = tp.__required_keys__ # type: ignore[attr-defined, attribute-error] # mypy, pytype
|
||||
except AttributeError:
|
||||
# {typing in Python 3.8, mypy_extensions}.TypedDict
|
||||
if options.strict:
|
||||
if sys.version_info[:2] >= (3, 9):
|
||||
advise = "Suggest use a typing.TypedDict instead."
|
||||
else:
|
||||
advise = "Suggest use a typing_extensions.TypedDict instead."
|
||||
advise2 = f"Or use {options.funcname}(..., strict=False)."
|
||||
raise TypeNotSupportedError(
|
||||
f"{options.funcname} cannot determine which keys are required "
|
||||
f"and which are potentially-missing for the "
|
||||
f"specified kind of TypedDict. {advise} {advise2}"
|
||||
)
|
||||
else:
|
||||
if tp.__total__: # type: ignore[attr-defined, attribute-error] # mypy, pytype
|
||||
required_keys = resolved_annotations.keys()
|
||||
else:
|
||||
required_keys = frozenset()
|
||||
|
||||
for k, v in value.items(): # type: ignore[attribute-error] # pytype
|
||||
V = resolved_annotations.get(k, _MISSING)
|
||||
if V is not _MISSING:
|
||||
e = _checkcast_inner(V, v, options)
|
||||
if e is not None:
|
||||
return ValidationError(
|
||||
tp,
|
||||
value,
|
||||
_causes=[e._with_prefix(_LazyStr(lambda: f"At key {k!r}"))],
|
||||
)
|
||||
|
||||
for k in required_keys:
|
||||
if k not in value: # type: ignore[unsupported-operands] # pytype
|
||||
return ValidationError(
|
||||
tp,
|
||||
value,
|
||||
_causes=[
|
||||
ValidationError._from_message(
|
||||
_LazyStr(lambda: f"Required key {k!r} is missing")
|
||||
)
|
||||
],
|
||||
)
|
||||
return None
|
||||
else:
|
||||
return ValidationError(tp, value)
|
||||
|
||||
if _is_newtype(tp):
|
||||
if options.strict:
|
||||
supertype_repr = type_repr(tp.__supertype__) # type: ignore[attr-defined, attribute-error] # mypy, pytype
|
||||
tp_name_repr = repr(tp.__name__) # type: ignore[attr-defined] # mypy
|
||||
raise TypeNotSupportedError(
|
||||
f"{options.funcname} cannot reliably determine whether value is "
|
||||
f"a NewType({tp_name_repr}, {supertype_repr}) because "
|
||||
f"NewType wrappers are erased at runtime "
|
||||
f"and are indistinguishable from their supertype. "
|
||||
f"Consider using {options.funcname}(..., strict=False) to treat "
|
||||
f"NewType({tp_name_repr}, {supertype_repr}) "
|
||||
f"like {supertype_repr}."
|
||||
)
|
||||
else:
|
||||
supertype = tp.__supertype__ # type: ignore[attr-defined, attribute-error] # mypy, pytype
|
||||
return _checkcast_inner(supertype, value, options)
|
||||
|
||||
if isinstance(tp, TypeVar): # type: ignore[wrong-arg-types] # pytype
|
||||
raise TypeNotSupportedError(
|
||||
f"{options.funcname} cannot reliably determine whether value matches a TypeVar."
|
||||
)
|
||||
|
||||
if tp is Any:
|
||||
return None
|
||||
|
||||
if tp is Never or tp is NoReturn:
|
||||
return ValidationError(tp, value)
|
||||
|
||||
if isinstance(tp, TypeAliasType): # type: ignore[16] # pyre
|
||||
if len(tp.__type_params__) > 0: # type: ignore[16] # pyre
|
||||
substitutions = dict(
|
||||
zip(tp.__type_params__, ((Any,) * len(tp.__type_params__)))
|
||||
)
|
||||
new_tp = _substitute(tp.__value__, substitutions)
|
||||
else:
|
||||
new_tp = tp.__value__
|
||||
return _checkcast_inner(new_tp, value, options) # type: ignore[16] # pyre
|
||||
|
||||
if isinstance(tp, ForwardRef):
|
||||
raise UnresolvedForwardRefError()
|
||||
|
||||
if isinstance(value, tp): # type: ignore[arg-type, wrong-arg-types] # mypy, pytype
|
||||
return None
|
||||
else:
|
||||
return ValidationError(tp, value)
|
||||
|
||||
@functools.lru_cache()
|
||||
def eval_type_str(tp: str, /) -> object:
|
||||
"""
|
||||
Resolves a string-reference to a type that can be imported,
|
||||
such as `'typing.List'`.
|
||||
|
||||
This function does internally cache lookups that have been made in
|
||||
the past to improve performance. If you need to clear this cache
|
||||
you can call:
|
||||
|
||||
eval_type_str.cache_clear()
|
||||
|
||||
Note that this function's implementation uses eval() internally.
|
||||
|
||||
Raises:
|
||||
* UnresolvableTypeError --
|
||||
If the specified string-reference could not be resolved to a type.
|
||||
"""
|
||||
if not isinstance(tp, str): # pragma: no cover
|
||||
raise ValueError()
|
||||
|
||||
# Determine which module to lookup the type from
|
||||
mod: ModuleType
|
||||
module_name: str
|
||||
member_expr: str
|
||||
m = _IMPORTABLE_TYPE_EXPRESSION_RE.fullmatch(tp)
|
||||
if m is not None:
|
||||
(module_name_dot, member_expr) = m.groups()
|
||||
module_name = module_name_dot[:-1]
|
||||
try:
|
||||
mod = importlib.import_module(module_name)
|
||||
except Exception:
|
||||
raise UnresolvableTypeError(
|
||||
f"Could not resolve type {tp!r}: " f"Could not import {module_name!r}."
|
||||
)
|
||||
else:
|
||||
m = _UNIMPORTABLE_TYPE_EXPRESSION_RE.fullmatch(tp)
|
||||
if m is not None:
|
||||
mod = _BUILTINS_MODULE
|
||||
module_name = _BUILTINS_MODULE.__name__
|
||||
member_expr = tp
|
||||
else:
|
||||
raise UnresolvableTypeError(
|
||||
f"Could not resolve type {tp!r}: "
|
||||
f"{tp!r} does not appear to be a valid type."
|
||||
)
|
||||
|
||||
# Lookup the type from a module
|
||||
try:
|
||||
member = eval(member_expr, mod.__dict__, None)
|
||||
except Exception:
|
||||
raise UnresolvableTypeError(
|
||||
f"Could not resolve type {tp!r}: "
|
||||
f"Could not eval {member_expr!r} inside module {module_name!r}."
|
||||
f"{_EXTRA_ADVISE_IF_MOD_IS_BUILTINS if mod is _BUILTINS_MODULE else ''}"
|
||||
)
|
||||
|
||||
# Interpret an imported str as a TypeAlias
|
||||
if isinstance(member, str):
|
||||
member = ForwardRef(member, is_argument=False)
|
||||
|
||||
# Resolve any ForwardRef instances inside the type
|
||||
try:
|
||||
member = eval_type(member, mod.__dict__, None) # type: ignore[16] # pyre
|
||||
except Exception:
|
||||
raise UnresolvableTypeError(
|
||||
f"Could not resolve type {tp!r}: "
|
||||
f"Could not eval type {member!r} inside module {module_name!r}."
|
||||
f"{_EXTRA_ADVISE_IF_MOD_IS_BUILTINS if mod is _BUILTINS_MODULE else ''}"
|
||||
)
|
||||
|
||||
# 1. Ensure the object is actually a type
|
||||
# 2. As a special case, interpret None as type(None)
|
||||
try:
|
||||
member = _type_check(member, f"Could not resolve type {tp!r}: ") # type: ignore[16] # pyre
|
||||
except TypeError as e:
|
||||
raise UnresolvableTypeError(str(e))
|
||||
return member
|
||||
|
9
test_.py
Normal file
9
test_.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
import std.typecheck as tc
|
||||
import typing
|
||||
OptionalString = str | None
|
||||
print(tc.checktype("wow", OptionalString))
|
||||
print(tc.checktype(None, OptionalString))
|
||||
print(tc.checktype(0xDEADBEEF, OptionalString))
|
||||
@tc.check_args
|
||||
def a(a:int,b:str,c:typing.Any):...
|
||||
a(1,"",1223)
|
Loading…
Add table
Add a link
Reference in a new issue