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):
|
def infix(name: str):
|
||||||
yield tokenize.OP, ">>"
|
yield tokenize.OP, ">>"
|
||||||
yield tokenize.NAME, name
|
yield tokenize.NAME, name
|
||||||
|
@ -147,7 +147,7 @@ def translate(file: io.StringIO, debug: int = 0):
|
||||||
yield type,name
|
yield type,name
|
||||||
dprint(f'---END DEBOUT---', 4)
|
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')
|
dir = Path('dist')
|
||||||
if input_path.is_dir():
|
if input_path.is_dir():
|
||||||
for i in input_path.glob('*'):
|
for i in input_path.glob('*'):
|
||||||
|
@ -176,17 +176,19 @@ app = typer.Typer()
|
||||||
|
|
||||||
verbosity_arg = typing.Annotated[int, typer.Option('--verbosity', '-v')]
|
verbosity_arg = typing.Annotated[int, typer.Option('--verbosity', '-v')]
|
||||||
minify_arg = typing.Annotated[bool, typer.Option('--minify', '-m')]
|
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('t')
|
||||||
@app.command('ts')
|
@app.command('ts')
|
||||||
@app.command('transpile')
|
@app.command('transpile')
|
||||||
def transpile_cmd(input_path: pathlib.Path, verbosity: verbosity_arg = 0, minify: minify_arg = False) -> None:
|
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)
|
transpile(input_path, verbosity, minify, bcp)
|
||||||
@app.command('r')
|
@app.command('r')
|
||||||
@app.command('run')
|
@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)
|
input_path = Path(input_path)
|
||||||
transpile(input_path, verbosity, minify)
|
transpile(input_path, verbosity, minify, bcp)
|
||||||
if input_path.is_dir():
|
if input_path.is_dir():
|
||||||
os.system(f'{sys.executable} -m dist')
|
os.system(f'{sys.executable} -m dist')
|
||||||
Path('dist').rmtree()
|
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('sentinels', std'sentinels.py')
|
||||||
_INTERNAL_add_fakeimport('ipathlib', std'ipathlib.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_lazymerge = _INTERNAL_Token(lambda lhs, rhs: _INTERNAL_LazyIterable(lhs, rhs))
|
||||||
|
|
||||||
_INTERNAL_lpipe = _INTERNAL_Token(lambda lhs, rhs: rhs(lhs))
|
_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