diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 1f0215b..cd12619 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -8,8 +8,6 @@ jobs: strategy: matrix: include: - - python-version: "3.8" - usable-python-version: "3.8" - python-version: "3.9" usable-python-version: "3.9" - python-version: "3.10" @@ -18,6 +16,8 @@ jobs: usable-python-version: "3.11" - python-version: "3.12" usable-python-version: "3.12" + - python-version: "3.13" + usable-python-version: "3.13" steps: - uses: actions/checkout@v4 @@ -26,7 +26,7 @@ jobs: python-version: ${{ matrix.python-version }} - run: ./pw poetry config virtualenvs.in-project true - name: Set up cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache with: path: .venv @@ -44,10 +44,10 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.9" - run: ./pw poetry config virtualenvs.in-project true - name: Set up cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache with: path: .venv diff --git a/.idea/basedtyping.iml b/.idea/basedtyping.iml index 7ad5deb..e83e1dc 100644 --- a/.idea/basedtyping.iml +++ b/.idea/basedtyping.iml @@ -3,8 +3,8 @@ - + diff --git a/basedtyping/__init__.py b/basedtyping/__init__.py index b871c15..868bbf7 100644 --- a/basedtyping/__init__.py +++ b/basedtyping/__init__.py @@ -5,15 +5,15 @@ from __future__ import annotations import ast -import typing import sys +import typing +import warnings from types import FunctionType from typing import ( # type: ignore[attr-defined] TYPE_CHECKING, Any, Callable, Final, - ForwardRef, Generic, NoReturn, Sequence, @@ -25,15 +25,22 @@ _remove_dups_flatten, _SpecialForm, _tp_cache, - _type_check, cast, ) import typing_extensions -from typing_extensions import Never, ParamSpec, Self, TypeAlias, TypeGuard, TypeVarTuple +from typing_extensions import Never, ParamSpec, Self, TypeAlias, TypeGuard, TypeVarTuple, override +from basedtyping import transformer from basedtyping.runtime_only import OldUnionType +BASEDMYPY_TYPE_CHECKING: Final = False +"""a special constant, is always `False`, but basedmypy will always assume it to be true + +if you aren't using basedmypy, you may have to configure your type checker to consider + this variable "always false" +""" + if not TYPE_CHECKING: if sys.version_info >= (3, 11): from typing import _collect_parameters @@ -57,6 +64,8 @@ "Intersection", "TypeForm", "as_functiontype", + "ForwardRef", + "BASEDMYPY_TYPE_CHECKING", ) if TYPE_CHECKING: @@ -68,13 +77,15 @@ class _BasedSpecialForm(_SpecialForm, _root=True): # type: ignore[misc] _name: str - def __init_subclass__(cls, _root=False): # noqa: FBT002 + @override + def __init_subclass__(cls, _root=False): super().__init_subclass__(_root=_root) # type: ignore[call-arg] def __init__(self, *args: object, **kwargs: object): self.alias = kwargs.pop("alias", _BasedGenericAlias) super().__init__(*args, **kwargs) + @override def __repr__(self) -> str: return "basedtyping." + self._name @@ -84,12 +95,6 @@ def __and__(self, other: object) -> object: def __rand__(self, other: object) -> object: return Intersection[other, self] - if sys.version_info < (3, 9): - - @_tp_cache_typed - def __getitem__(self, item: object) -> object: - return self.alias(self, item) # type: ignore[operator] - class _BasedGenericAlias(_GenericAlias, _root=True): def __and__(self, other: object) -> object: @@ -249,6 +254,7 @@ def _is_subclass(cls, subclass: object) -> TypeGuard[_ReifiedGenericMetaclass]: cast(_ReifiedGenericMetaclass, subclass)._orig_class(), ) + @override def __subclasscheck__(cls, subclass: object) -> bool: if not cls._is_subclass(subclass): return False @@ -269,6 +275,7 @@ def __subclasscheck__(cls, subclass: object) -> bool: subclass._check_generics_reified() return cls._type_var_check(subclass.__reified_generics__) + @override def __instancecheck__(cls, instance: object) -> bool: if not cls._is_subclass(type(instance)): return False @@ -277,6 +284,7 @@ def __instancecheck__(cls, instance: object) -> bool: return cls._type_var_check(cast(ReifiedGeneric[object], instance).__reified_generics__) # need the generic here for pyright. see https://github.com/microsoft/pyright/issues/5488 + @override def __call__(cls: type[T], *args: object, **kwargs: object) -> T: """A placeholder ``__call__`` method that gets called when the class is instantiated directly, instead of first supplying the type parameters. @@ -405,6 +413,7 @@ def __class_getitem__( # type: ignore[no-any-decorated] reified_generic_copy._can_do_instance_and_subclass_checks_without_generics = False return reified_generic_copy + @override def __init_subclass__(cls): cls._can_do_instance_and_subclass_checks_without_generics = True super().__init_subclass__() @@ -459,11 +468,7 @@ def issubform(form: _Forms, forminfo: _Forms) -> bool: return issubclass(form, forminfo) # type: ignore[arg-type] -if TYPE_CHECKING: - # We pretend that it's an alias to Any so that it's slightly more compatible with - # other tools, basedmypy will still utilize the SpecialForm over the TypeAlias. - Untyped: TypeAlias = Any # type: ignore[no-any-explicit] -elif sys.version_info >= (3, 9): +if BASEDMYPY_TYPE_CHECKING or not TYPE_CHECKING: @_BasedSpecialForm def Untyped( # noqa: N802 @@ -475,26 +480,24 @@ def Untyped( # noqa: N802 This is more specialized than ``Any`` and can help with gradually typing modules. """ raise TypeError(f"{self} is not subscriptable") - else: - Untyped: Final = _BasedSpecialForm( - "Untyped", - doc=( - "Special type indicating that something isn't typed.\nThis is more" - " specialized than ``Any`` and can help with gradually typing modules." - ), - ) + # We pretend that it's an alias to Any so that it's slightly more compatible with + # other tools + Untyped: TypeAlias = Any # type: ignore[no-any-explicit] class _IntersectionGenericAlias(_BasedGenericAlias, _root=True): + @override def copy_with(self, args: object) -> Self: # type: ignore[override] # TODO: put in the overloads # noqa: TD003 return cast(Self, Intersection[args]) + @override def __eq__(self, other: object) -> bool: if not isinstance(other, _IntersectionGenericAlias): return NotImplemented return set(self.__args__) == set(other.__args__) + @override def __hash__(self) -> int: return hash(frozenset(self.__args__)) @@ -504,63 +507,61 @@ def __instancecheck__(self, obj: object) -> bool: def __subclasscheck__(self, cls: type[object]) -> bool: return all(issubclass(cls, arg) for arg in self.__args__) + @override def __reduce__(self) -> (object, object): func, (_, args) = super().__reduce__() # type: ignore[no-any-expr, misc] return func, (Intersection, args) -if sys.version_info > (3, 9): +@_BasedSpecialForm +def Intersection(self: _BasedSpecialForm, parameters: object) -> object: # noqa: N802 + """Intersection type; Intersection[X, Y] means both X and Y. - @_BasedSpecialForm - def Intersection(self: _BasedSpecialForm, parameters: object) -> object: # noqa: N802 - """Intersection type; Intersection[X, Y] means both X and Y. - - To define an intersection: - - If using __future__.annotations, shortform can be used e.g. A & B - - otherwise the fullform must be used e.g. Intersection[A, B]. + To define an intersection: + - If using __future__.annotations, shortform can be used e.g. A & B + - otherwise the fullform must be used e.g. Intersection[A, B]. - Details: - - The arguments must be types and there must be at least one. - - None as an argument is a special case and is replaced by - type(None). - - Intersections of intersections are flattened, e.g.:: + Details: + - The arguments must be types and there must be at least one. + - None as an argument is a special case and is replaced by + type(None). + - Intersections of intersections are flattened, e.g.:: - Intersection[Intersection[int, str], float] == Intersection[int, str, float] + Intersection[Intersection[int, str], float] == Intersection[int, str, float] - - Intersections of a single argument vanish, e.g.:: + - Intersections of a single argument vanish, e.g.:: - Intersection[int] == int # The constructor actually returns int + Intersection[int] == int # The constructor actually returns int - - Redundant arguments are skipped, e.g.:: + - Redundant arguments are skipped, e.g.:: - Intersection[int, str, int] == Intersection[int, str] + Intersection[int, str, int] == Intersection[int, str] - - When comparing intersections, the argument order is ignored, e.g.:: + - When comparing intersections, the argument order is ignored, e.g.:: - Intersection[int, str] == Intersection[str, int] - - - You cannot subclass or instantiate an intersection. - """ - if parameters == (): - raise TypeError("Cannot take an Intersection of no types.") - if not isinstance(parameters, tuple): - parameters = (parameters,) - msg = "Intersection[arg, ...]: each arg must be a type." - parameters = tuple(_type_check(p, msg) for p in parameters) # type: ignore[no-any-expr] - parameters = _remove_dups_flatten(parameters) # type: ignore[no-any-expr] - if len(parameters) == 1: # type: ignore[no-any-expr] - return parameters[0] # type: ignore[no-any-expr] - return _IntersectionGenericAlias(self, parameters) # type: ignore[arg-type, no-any-expr] + Intersection[int, str] == Intersection[str, int] -else: - Intersection = _BasedSpecialForm("Intersection", doc="", alias=_IntersectionGenericAlias) + - You cannot subclass or instantiate an intersection. + """ + if parameters == (): + raise TypeError("Cannot take an Intersection of no types.") + if not isinstance(parameters, tuple): + parameters = (parameters,) + msg = "Intersection[arg, ...]: each arg must be a type." + parameters = tuple(_type_check(p, msg) for p in parameters) + parameters = _remove_dups_flatten(parameters) # type: ignore[no-any-expr] + if len(parameters) == 1: # type: ignore[no-any-expr] + return parameters[0] # type: ignore[no-any-expr] + return _IntersectionGenericAlias(self, parameters) # type: ignore[arg-type, no-any-expr] class _TypeFormForm(_BasedSpecialForm, _root=True): # type: ignore[misc] + # TODO: decorator-ify # noqa: TD003 def __init__(self, doc: str): self._name = "TypeForm" self._doc = self.__doc__ = doc + @override def __getitem__(self, parameters: object | tuple[object]) -> _BasedGenericAlias: if not isinstance(parameters, tuple): parameters = (parameters,) @@ -582,7 +583,7 @@ def f[T](t: TypeForm[T]) -> T: ... # TODO: conditionally declare FunctionType with a BASEDMYPY so that this doesn't break everyone else # https://github.com/KotlinIsland/basedmypy/issues/524 -def as_functiontype(fn: Callable[P, T]) -> FunctionType[P, T]: # type: ignore[type-arg] +def as_functiontype(fn: Callable[P, T]) -> FunctionType[P, T]: """Asserts that a ``Callable`` is a ``FunctionType`` and returns it best used as a decorator to fix other incorrectly typed decorators: @@ -593,9 +594,10 @@ def deco(fn: Callable[[], None]) -> Callable[[], None]: ... @deco def foo(): ... """ - if not isinstance(fn, FunctionType): # type: ignore[redundant-expr] + if not isinstance(fn, FunctionType): raise TypeError(f"{fn} is not a FunctionType") - return fn # type: ignore[unreachable] + # https://github.com/KotlinIsland/basedmypy/issues/745 + return cast("FunctionType[P, T]", fn) class ForwardRef(typing.ForwardRef, _root=True): # type: ignore[call-arg,misc] @@ -606,26 +608,36 @@ class ForwardRef(typing.ForwardRef, _root=True): # type: ignore[call-arg,misc] if the original syntax is not supported in the current Python version. """ - def __init__(self, arg, *, is_argument=True, module=None, is_class=False): - if not isinstance(arg, str): + # older typing.ForwardRef doesn't have this + if sys.version_info < (3, 10): + __slots__ = ("__forward_module__", "__forward_is_class__") + elif sys.version_info < (3, 11): + __slots__ = ("__forward_is_class__",) + + def __init__(self, arg: str, *, is_argument=True, module: object = None, is_class=False): + if not isinstance(arg, str): # type: ignore[redundant-expr] raise TypeError(f"Forward reference must be a string -- got {arg!r}") # If we do `def f(*args: *Ts)`, then we'll have `arg = '*Ts'`. # Unfortunately, this isn't a valid expression on its own, so we # do the unpacking manually. - if arg.startswith("*"): - arg_to_compile = f"({arg},)[0]" # E.g. (*Ts,)[0] or (*tuple[int, int],)[0] - else: - arg_to_compile = arg + arg_to_compile = ( + f"({arg},)[0]" # E.g. (*Ts,)[0] or (*tuple[int, int],)[0] + if arg.startswith("*") + else arg + ) try: - code = compile(arg_to_compile, "", "eval") + with warnings.catch_warnings(): + # warnings come from some based syntax, i can't remember what + warnings.simplefilter("ignore", category=SyntaxWarning) + code = compile(arg_to_compile, "", "eval") except SyntaxError: - code = compile( - ast.parse(arg.removeprefix("def").lstrip(), mode="func_type"), - "", - "func_type", - ast.PyCF_ONLY_AST, - ) + try: + ast.parse(arg_to_compile.removeprefix("def "), mode="func_type") + except SyntaxError: + raise SyntaxError(f"invalid syntax in ForwardRef: {arg_to_compile}?") from None + else: + code = compile("'un-representable callable type'", "", "eval") self.__forward_arg__ = arg self.__forward_code__ = code @@ -635,10 +647,67 @@ def __init__(self, arg, *, is_argument=True, module=None, is_class=False): self.__forward_is_class__ = is_class self.__forward_module__ = module - def _evaluate( - self, - globalns: dict[str, Any] | None, - localns: dict[str, Any] | None, - recursive_guard: frozenset[str] | None = None, - ) -> Any: - return typing.t_eval_direct(self, globalns, localns) + if sys.version_info >= (3, 13): + + @override + def _evaluate( + self, + globalns: dict[str, object] | None, + localns: dict[str, object] | None, + type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] = (), + *, + recursive_guard: frozenset[str], + ) -> object | None: + return transformer._eval_direct(self, globalns, localns) + + elif sys.version_info >= (3, 12): + + @override + def _evaluate( + self, + globalns: dict[str, object] | None, + localns: dict[str, object] | None, + type_params: tuple[TypeVar | typing.ParamSpec | typing.TypeVarTuple, ...] | None = None, + *, + recursive_guard: frozenset[str], + ) -> object | None: + return transformer._eval_direct(self, globalns, localns) + + else: + + @override + def _evaluate( + self, + globalns: dict[str, object] | None, + localns: dict[str, object] | None, + recursive_guard: frozenset[str], + ) -> object | None: + return transformer._eval_direct(self, globalns, localns) + + +def _type_check(arg: object, msg: str) -> object: + """Check that the argument is a type, and return it (internal helper). + + As a special case, accept None and return type(None) instead. Also wrap strings + into ForwardRef instances. Consider several corner cases, for example plain + special forms like Union are not valid, while Union[int, str] is OK, etc. + The msg argument is a human-readable error message, e.g:: + + "Union[arg, ...]: arg should be a type." + + We append the repr() of the actual value (truncated to 100 chars). + """ + invalid_generic_forms = (Generic, typing.Protocol) + + arg = _type_convert(arg) + if isinstance(arg, _GenericAlias) and arg.__origin__ in invalid_generic_forms: # type: ignore[comparison-overlap] + raise TypeError(f"{arg} is not valid as type argument") + if arg in (Any, NoReturn, typing.Final, Untyped): + return arg + if isinstance(arg, _SpecialForm) or arg in (Generic, typing.Protocol): + raise TypeError(f"Plain {arg} is not valid as type argument") + if isinstance(arg, (type, TypeVar, ForwardRef)): + return arg + if not callable(arg): + raise TypeError(f"{msg} Got {arg!r:.100}.") + return arg diff --git a/basedtyping/transformer.py b/basedtyping/transformer.py index da867a9..56de633 100644 --- a/basedtyping/transformer.py +++ b/basedtyping/transformer.py @@ -1,25 +1,30 @@ +"""utilities to create standard compatible annotations""" + from __future__ import annotations import ast +import sys import types import typing -import uuid from contextlib import contextmanager from enum import Enum -from typing import Any +from functools import partial +from typing import cast + +import typing_extensions +from typing_extensions import override + import basedtyping +# ruff: noqa: S101 class CringeTransformer(ast.NodeTransformer): - """ - Transforms `1 | 2` into `Literal[1] | Literal[2]` etc - TODO: list[(int, str)] -> list[tuple[int, str]], is this even possible with ast? check src - """ + """Transforms `1 | 2` into `Literal[1] | Literal[2]` etc""" def __init__( self, - globalns: dict[str, Any] | None, - localns: dict[str, Any] | None, + globalns: dict[str, object] | None, + localns: dict[str, object] | None, *, string_literals: bool, ): @@ -29,114 +34,157 @@ def __init__( if globalns is None and localns is None: globalns = localns = {} elif globalns is None: - # apparently pyright doesn't infer this automatically assert localns is not None globalns = localns elif localns is None: - # apparently pyright doesn't infer this automatically assert globalns is not None localns = globalns - self.typing_name = f"typing_extensions_{uuid.uuid4().hex}" - self.basedtyping_name = f"basedtyping_{uuid.uuid4().hex}" + fair_and_unique_uuid_roll = "c4357574960843a2a8f9eb0c11aa88e5" + self.typing_name = f"_typing_extensions_{fair_and_unique_uuid_roll}" + self.basedtyping_name = f"_basedtyping_{fair_and_unique_uuid_roll}" self.globalns = globalns - import typing_extensions - self.localns = { - **localns, + self.localns = localns | { self.typing_name: typing_extensions, self.basedtyping_name: basedtyping, } + @override + def visit(self, node: ast.AST) -> ast.AST: + return cast(ast.AST, super().visit(node)) + def eval_type( - self, node: ast.Expression | ast.expr, *, original_ref: typing.ForwardRef | None = None + self, + node: ast.FunctionType | ast.Expression | ast.expr, + *, + original_ref: typing.ForwardRef | None = None, ) -> object: - if not isinstance(node, ast.Expression): + if isinstance(node, ast.expr): node = ast.copy_location(ast.Expression(node), node) ref = typing.ForwardRef(ast.dump(node)) if original_ref: for attr in ("is_argument", " is_class", "module"): attr = f"__forward_{attr}__" if hasattr(original_ref, attr): - setattr(ref, attr, getattr(original_ref, attr)) - ref.__forward_code__ = compile(node, "", "eval") + setattr(ref, attr, cast(object, getattr(original_ref, attr))) + if not isinstance(node, ast.FunctionType): + ref.__forward_code__ = compile(node, "", "eval") try: - return typing._eval_type(ref, self.globalns, self.localns) + type_ = typing._type_convert( # type: ignore[attr-defined] + cast(object, eval(ref.__forward_code__, self.globalns, self.localns)) # noqa: S307 + ) + if sys.version_info > (3, 12): + return typing._eval_type( # type: ignore[attr-defined] + type_, self.globalns, self.localns, type_params=() + ) + return typing._eval_type( # type: ignore[attr-defined] + type_, self.globalns, self.localns + ) except TypeError: return None - def _typing(self, attr: str): + def _typing(self, attr: str) -> ast.Attribute: result = ast.Attribute( value=ast.Name(id=self.typing_name, ctx=ast.Load()), attr=attr, ctx=ast.Load() ) return ast.fix_missing_locations(result) - def _basedtyping(self, attr: str): + def _basedtyping(self, attr: str) -> ast.Attribute: result = ast.Attribute( value=ast.Name(id=self.basedtyping_name, ctx=ast.Load()), attr=attr, ctx=ast.Load() ) return ast.fix_missing_locations(result) - def _literal(self, value: ast.Constant | ast.Name | ast.Attribute): + def _literal(self, value: ast.Constant | ast.Name | ast.Attribute) -> ast.Subscript: return self.subscript(self._typing("Literal"), value) - def subscript(self, value, slice): - result = ast.Subscript(value=value, slice=slice, ctx=ast.Load()) + def subscript(self, value: ast.expr, slice_: ast.expr) -> ast.Subscript: + result = ast.Subscript(value=value, slice=slice_, ctx=ast.Load()) return ast.fix_missing_locations(result) _implicit_tuple = False @contextmanager - def implicit_tuple(self): + def implicit_tuple(self, *, value=True) -> typing.Iterator[None]: implicit_tuple = self._implicit_tuple - self._implicit_tuple = True + self._implicit_tuple = value try: yield finally: self._implicit_tuple = implicit_tuple + @override def visit_Subscript(self, node: ast.Subscript) -> ast.AST: + node_type = self.eval_type(node.value) + if self.eval_type(node.value) is typing_extensions.Literal: + return node + if node_type is typing_extensions.Annotated: + slice_ = node.slice + if isinstance(slice_, ast.Tuple): + temp = self.visit(slice_.elts[0]) + assert isinstance(temp, ast.expr) + slice_.elts[0] = temp + else: + temp = self.visit(slice_) + assert isinstance(temp, ast.expr) + node.slice = temp + return node with self.implicit_tuple(): - node = self.generic_visit(node) - # TODO: FunctionType -> Callable + result = self.generic_visit(node) + assert isinstance(result, ast.Subscript) + node = result + node_type = self.eval_type(node.value) if node_type is types.FunctionType: - node = self.subscript(self._typing("Callable"), node.slice) + slice2_ = node.slice + node = self.subscript(self._typing("Callable"), slice2_) return node - def visit_Attribute(self, node) -> ast.Name: + @override + def visit_Attribute(self, node: ast.Attribute) -> ast.AST: node = self.generic_visit(node) + assert isinstance(node, ast.expr) node_type = self.eval_type(node) if isinstance(node_type, Enum): - node = self._literal(node) + assert isinstance(node, (ast.Name, ast.Attribute)) + return self._literal(node) return node - def visit_Name(self, node) -> ast.Name: - node = self.generic_visit(node) + @override + def visit_Name(self, node: ast.Name) -> ast.AST: name_type = self.eval_type(node) if isinstance(name_type, Enum): - node = self._literal(node) + return self._literal(node) return node + @override def visit_Constant(self, node: ast.Constant) -> ast.AST: - node = self.generic_visit(node) - if isinstance(node.value, int | bool) or ( - self.string_literals and isinstance(node.value, str) - ): - node = self._literal(node) + value = cast(object, node.value) + if not self.string_literals and isinstance(value, str): + return self._transform(basedtyping.ForwardRef(value)).body + if isinstance(value, int) or (self.string_literals and isinstance(value, str)): + return self._literal(node) return node + @override def visit_Tuple(self, node: ast.Tuple) -> ast.AST: - node = self.generic_visit(node) + with self.implicit_tuple(value=False): + result = self.generic_visit(node) if not self._implicit_tuple: - return self.subscript(self._typing("Tuple"), node) - return node + return self.subscript(self._typing("Tuple"), cast(ast.expr, result)) + return result + @override def visit_Compare(self, node: ast.Compare) -> ast.AST: if len(node.ops) == 1 and isinstance(node.ops[0], ast.Is): - node = self.subscript(self._typing("TypeIs"), self.generic_visit(node.comparators[0])) + result = self.subscript( + self._typing("TypeIs"), cast(ast.expr, self.generic_visit(node.comparators[0])) + ) + return self.generic_visit(result) return self.generic_visit(node) + @override def visit_IfExp(self, node: ast.IfExp) -> ast.AST: if ( isinstance(node.body, ast.Compare) @@ -144,53 +192,71 @@ def visit_IfExp(self, node: ast.IfExp) -> ast.AST: and isinstance(node.body.ops[0], ast.Is) ): node.body = self.subscript( - self._typing("TypeGuard"), self.generic_visit(node.body.comparators[0]) + self._typing("TypeGuard"), + cast(ast.expr, self.generic_visit(node.body.comparators[0])), ) return self.generic_visit(node) - def visit_FunctionType(self, node: ast.FunctionType) -> ast.AST: + def visit_FunctionType(self, node: ast.FunctionType) -> ast.AST: # noqa: N802 https://github.com/KotlinIsland/basedmypy/issues/763 node = self.generic_visit(node) - return self.subscript( - self._typing("Callable"), - ast.Tuple([ast.List(node.argtypes, ctx=ast.Load()), node.returns], ctx=ast.Load()), + assert isinstance(node, ast.FunctionType) + return ast.Expression( + self.subscript( + self._typing("Callable"), + ast.Tuple([ast.List(node.argtypes, ctx=ast.Load()), node.returns], ctx=ast.Load()), + ) ) + @override def visit_BinOp(self, node: ast.BinOp) -> ast.AST: node = self.generic_visit(node) - if isinstance(node.op, ast.BitAnd): + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitAnd): node = self.subscript( self._basedtyping("Intersection"), ast.Tuple([node.left, node.right], ctx=ast.Load()), ) return node + def _transform(self, value: typing.ForwardRef) -> ast.Expression: + tree: ast.AST + try: + tree = ast.parse(value.__forward_arg__, mode="eval") + except SyntaxError: + arg = value.__forward_arg__.lstrip() + if arg.startswith(("def ", "def(")): + arg = arg[3:].lstrip() + tree = ast.parse(arg, mode="func_type") -def _eval_direct( - value: typing.ForwardRef, - globalns: dict[str, Any] | None = None, - localns: dict[str, Any] | None = None, -): - return eval_type_based(value, globalns, localns, string_literals=False) + tree = self.visit(tree) + assert isinstance(tree, ast.Expression) + return tree def eval_type_based( value: object, - globalns: typing.Mapping[str, object] | None = None, - localns: typing.Mapping[str, object] | None = None, + globalns: dict[str, object] | None = None, + localns: dict[str, object] | None = None, *, string_literals: bool, ) -> object: + """Like `typing._eval_type`, but supports based typing features. + Specifically, this transforms `1 | 2` into `typing.Union[Literal[1], Literal[2]]` + and `(int) -> str` into `typing.Callable[[int], str]` etc. """ - Like `typing._eval_type`, but lets older Python versions use newer typing features. - Specifically, this transforms `X | Y` into `typing.Union[X, Y]` - and `list[X]` into `typing.List[X]` etc. (for all the types made generic in PEP 585) - if the original syntax is not supported in the current Python version. - """ - try: - tree = ast.parse(value.__forward_arg__, mode="eval") - except SyntaxError: - tree = ast.parse(value.__forward_arg__.removeprefix("def").lstrip(), mode="func_type") - + if not isinstance(value, typing.ForwardRef): + return value transformer = CringeTransformer(globalns, localns, string_literals=string_literals) - tree = transformer.visit(tree) + tree = transformer._transform(value) return transformer.eval_type(tree, original_ref=value) + + +if typing.TYPE_CHECKING: + + def _eval_direct( + value: object, # noqa: ARG001 + globalns: dict[str, object] | None = None, # noqa: ARG001 + localns: dict[str, object] | None = None, # noqa: ARG001 + ) -> object: + ... +else: + _eval_direct = partial(eval_type_based, string_literals=False) diff --git a/poetry.lock b/poetry.lock index 519d948..45261d5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,46 +1,46 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "basedmypy" -version = "2.4.0" +version = "2.6.0" description = "Based static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "basedmypy-2.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e805613405b64ddb7081a4a372f183abe30380007bd080d94fcc1a7d0c81375e"}, - {file = "basedmypy-2.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a04ff1b662cc1578c1d887fc59cc72a915eec3e68b6cc8f675e0784375b98f78"}, - {file = "basedmypy-2.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:931841c8fc91af4c52a9f19c3e490b6228993b908b58d14f5121c1fdc1910830"}, - {file = "basedmypy-2.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a0196b1008f20ddb2226a616d2c05da1ab9c8266af58519866a23f670f3d8791"}, - {file = "basedmypy-2.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:09d3cbef8ce8e0d089d684fa25c3aff329a23dd2a8e551ea5e988140546ed4fc"}, - {file = "basedmypy-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec412ade88d13318cd914aa90c698e95fbbd20278fabb3ec419d957c5e30b2f2"}, - {file = "basedmypy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a9fb8d964ad132e9bb5421a3c96d06a626e4122529c4b4ae06413f0f68c3bfc0"}, - {file = "basedmypy-2.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abe59aca8792f51927f75adf992bc584224179c169cc3d4845eb92061d32539e"}, - {file = "basedmypy-2.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c04c578ec0614c7902f02c36ea95a109ec1395fabbedafa67f397ffa7bc774f8"}, - {file = "basedmypy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:e2cc1361f8e783593cb0372d2143dfc4608c0866a616005c51efe3d8f5a98c02"}, - {file = "basedmypy-2.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ea0ebbcdb43ec70f76090879a1acf2bc41e32cd265f9e952ead4def94bf99ddc"}, - {file = "basedmypy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:81885610e7041db5ec237675cfeb1632c6bbb883057e0e9c3e8faefaea2ab424"}, - {file = "basedmypy-2.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c56bb2e40e60cd816781921e0eb66395892134238bb4ee4160824264fb393bd"}, - {file = "basedmypy-2.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518c27cca18dbb800be95b42432e48296eb559206c1e8d6c474f6d6f0ea09d30"}, - {file = "basedmypy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:32aeb7a69b045a67e618903a4ef74252f5b3684f0b936966b463c2e0d22ba9b8"}, - {file = "basedmypy-2.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba3bc3af4fe398c64f96f6822f7af8828b598b567dcfec69f9a15f6cba114f96"}, - {file = "basedmypy-2.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64221f110e11ba564d86bb26bb2e9b4cc76a81d6eb5818360d5cd0033a386076"}, - {file = "basedmypy-2.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68e7112c23d47497f5f5852b00bb0e6818e779aa9f7729327e4845acda4b8765"}, - {file = "basedmypy-2.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:79a5e6f4db6585f8e3f203f07d230c79488eb5431fba7a215c32fe322eb5e5da"}, - {file = "basedmypy-2.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:dcb2612fac5e707245fd15de8b3f7e0b78b4d71420e27b2d16e41eab62691891"}, - {file = "basedmypy-2.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8f9cd4f163d220bb09d1043dbfd1ca882b3ef5fb62a01886f41172d9b16aeb66"}, - {file = "basedmypy-2.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdb60adeda60c3669322f0730f1a37b951510843a3af02b78037291286375cd5"}, - {file = "basedmypy-2.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0edc0c13a273f7396c1b1fc7d943a0685c44e97ddc38fc858dbb56cb70cd9b7a"}, - {file = "basedmypy-2.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4fec90ea3644d8b4ad1cb6ce2ac1480b4b95df82bee2153485f35463a1315ecb"}, - {file = "basedmypy-2.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:42ffef7402591e7206cfda2a85b9c00df4709a613c34609c5758d54ba17fb224"}, - {file = "basedmypy-2.4.0-py3-none-any.whl", hash = "sha256:d72942f88c72c85fe9d13bd89c7c428d5b9a392b5c4a107e7fe323feec9e9098"}, - {file = "basedmypy-2.4.0.tar.gz", hash = "sha256:489daaa7d6e2fdfe3db068ea499390f18d00fda66bccb273aaffc95e0768ac22"}, + {file = "basedmypy-2.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2dde708c6e130df4d0ae5931095844cc4157ac0e1a99c676a023b1c31dbdccd4"}, + {file = "basedmypy-2.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bfaef963805a02c24153c3d8af89b508248cdedec63c937af01e94237902dbdd"}, + {file = "basedmypy-2.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf928660d50ce1a2182b9162d15dca3b370a82a054a54c9cb7ec90f4b1f1cdc2"}, + {file = "basedmypy-2.6.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1cadba3f48ce22e82b7f31ab6dcad1d74c47f7aaf25a4aa094b1468923dd1292"}, + {file = "basedmypy-2.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:5ee0b49c577d93d401b029eeeb028f130340ba05d6048ce6e6adcbc609846d3b"}, + {file = "basedmypy-2.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e5bfcd0cd73aa51cf263fa88d6225f4dce8ddf24789ac7007f3b4f7ab410edf3"}, + {file = "basedmypy-2.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e2dcade460f9837a84907c8dc2bb6186e3792779f482f09bd84ec160129b5ed"}, + {file = "basedmypy-2.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12829f2619b733013d7fc9dfca76cdeb8c33724eab9d54ac5db7e23b902a0e3"}, + {file = "basedmypy-2.6.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c16a1c36ba528d5922be8fbaa918616cfe82f041a6a8fb95ef0c580761818cc"}, + {file = "basedmypy-2.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb22d5814601883451539ef83d2e362f6cad02a109afcf2af2628320cfc5198f"}, + {file = "basedmypy-2.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:590688b27afec8303b8e42043cd7afaa6b60d0ac0bb894a49e16f541378adca8"}, + {file = "basedmypy-2.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0dbf81111b710ab8fc4e6ba014061d71bcf5951c405d73220c783bc283691bff"}, + {file = "basedmypy-2.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e36eeb0f63e02b5ff9828208803b711d755ba493aba5b237832e38a8f4837ad1"}, + {file = "basedmypy-2.6.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b9819ea425a769b9bdfd8568d8ef6451e4f169f8df39fdb9d5014ee0a4ba15f4"}, + {file = "basedmypy-2.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:74cecf63520469d49f360d4226701b4e563bfc911dc84747a9abf67a00133f8b"}, + {file = "basedmypy-2.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5adf4a2b095002ce976d44cf4a7740bc6e500227f393b9ad4f93996cb175904e"}, + {file = "basedmypy-2.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:974dd7432e0d4e1571aae6575d46745f5f4d86e22bd8aa638ec8eb37b7cd084b"}, + {file = "basedmypy-2.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c5990f1ceaf7d76f25fb10c63f26148e25ca53400f52bc1a380d3a73318fea"}, + {file = "basedmypy-2.6.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e67e45ebd5c97754302be36bce4c5c2d45176407e39151e3984d952f88e5224"}, + {file = "basedmypy-2.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:143db931ddca9cac9aafb4fc055acd2bb900320380723e2bd448b2070a88adbc"}, + {file = "basedmypy-2.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce962c7f2d63bf9e370a04ebd8b269de8f1d029199dbf1111623aea7b1e5e825"}, + {file = "basedmypy-2.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3f32b1281a2b52bb10a3db07c474e90d244cc423345b5586869f8b4baca3a91"}, + {file = "basedmypy-2.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03369993451fed0f26df2705b8aa2cfe9c8ef5e8750f217acf5d08c726dc4749"}, + {file = "basedmypy-2.6.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3ac52f6b5f9ac9d9c2ec8873ba5f40140fd1dcbb47eb24977173fa4469752898"}, + {file = "basedmypy-2.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4ec34e2f6de049854399e7ca66bd0b4fe0f54ca76e96bc5573897d81ecb159ea"}, + {file = "basedmypy-2.6.0-py3-none-any.whl", hash = "sha256:329244dfbdd0507e83f6813e1509849e60b75321d193fa156f6151fb564faa17"}, + {file = "basedmypy-2.6.0.tar.gz", hash = "sha256:6ff3607d6e0ef776b9c0c9fdb24706f96783686ec7805c3bc28dd19eb07e5dbd"}, ] [package.dependencies] -basedtyping = ">=0.0.3" +basedtyping = ">=0.1.4" mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.1.0" +typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] @@ -61,13 +61,13 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -97,24 +97,24 @@ files = [ [[package]] name = "packaging" -version = "23.2" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -123,13 +123,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pytest" -version = "8.0.0" +version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"}, - {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"}, + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [package.dependencies] @@ -137,36 +137,36 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.3.0,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "ruff" -version = "0.2.1" +version = "0.2.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"}, - {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"}, - {file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"}, - {file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"}, - {file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"}, - {file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"}, + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, + {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, + {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, + {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, + {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, ] [[package]] @@ -182,16 +182,16 @@ files = [ [[package]] name = "typing-extensions" -version = "4.9.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [metadata] lock-version = "2.0" -python-versions = "^3.8" -content-hash = "899de3ee043cd8b95fa649e78efb2b28ad73dc832bc6b27c807865d89f1e5726" +python-versions = "^3.9" +content-hash = "373319d480ce46b0a60e4c71b84a00e007ec2b264ec7e3d8608ae2238dd2e4f2" diff --git a/pyproject.toml b/pyproject.toml index 7e6bdba..b6d9f4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,11 +5,11 @@ authors = [ ] description = "Utilities for basedmypy" name = "basedtyping" -version = "0.1.4" +version = "0.1.5" [tool.poetry.dependencies] -python = "^3.8" -typing_extensions = "^4.1" +python = "^3.9" +typing_extensions = "^4.12.2" [tool.poetry.group.dev.dependencies] basedmypy = "^2" @@ -24,10 +24,8 @@ requires = ["poetry-core>=1.0.8"] main = ["poetry==1.7.1"] [tool.mypy] -python_version = 3.8 +python_version = 3.9 packages = ["basedtyping", "tests"] -# we can't use override until we bump the minimum typing_extensions or something -disable_error_code = ["explicit-override"] [tool.ruff.format] skip-magic-trailing-comma = true diff --git a/tests/test_as_functiontype.py b/tests/test_as_functiontype.py index 9b6dd3e..ef2a65e 100644 --- a/tests/test_as_functiontype.py +++ b/tests/test_as_functiontype.py @@ -8,4 +8,4 @@ def test_as_functiontype(): with pytest.raises(TypeError): as_functiontype(all) - assert as_functiontype(test_as_functiontype) is test_as_functiontype # type: ignore[comparison-overlap] + assert as_functiontype(test_as_functiontype) is test_as_functiontype diff --git a/tests/test_basedmypy_typechecking.py b/tests/test_basedmypy_typechecking.py new file mode 100644 index 0000000..b8fd606 --- /dev/null +++ b/tests/test_basedmypy_typechecking.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from basedtyping import BASEDMYPY_TYPE_CHECKING + +if bool(): # noqa: UP018 + # hide this from pytest + + if BASEDMYPY_TYPE_CHECKING: # noqa: SIM108 + _ = 1 + "" # type: ignore[operator] + else: + _ = 1 + "" diff --git a/tests/test_basedspecialform.py b/tests/test_basedspecialform.py index 8865e5b..8910c30 100644 --- a/tests/test_basedspecialform.py +++ b/tests/test_basedspecialform.py @@ -1,9 +1,5 @@ from __future__ import annotations -import sys - -import pytest - from basedtyping import Intersection, TypeForm, Untyped @@ -12,15 +8,6 @@ def test_basedgenericalias_intersection(): assert None & TypeForm[int] == Intersection[None, TypeForm[int]] -@pytest.mark.xfail( - sys.version_info >= (3, 9), - reason=""" -`typing._type_check` says: - - if arg in (Any, LiteralString, NoReturn, Never, Self, TypeAlias): - return arg -""", -) def test_basedspecialform_intersection(): - assert Untyped & None == Intersection[Untyped, None] # type: ignore[no-any-expr] - assert None & Untyped == Intersection[Untyped, None] # type: ignore[no-any-expr] + assert Untyped & None == Intersection[Untyped, None] + assert None & Untyped == Intersection[Untyped, None] diff --git a/tests/test_function_type.py b/tests/test_function_type.py index dd07b2b..6b7cdc3 100644 --- a/tests/test_function_type.py +++ b/tests/test_function_type.py @@ -35,5 +35,4 @@ def test_method_descriptor(): assert_function(str.join) def test_class_method_descriptor(): - # method signature contains `Any` - assert_function(dict.fromkeys) # type: ignore[no-any-expr] + assert_function(dict.fromkeys) diff --git a/tests/test_intersection.py b/tests/test_intersection.py index 0b1990a..3f8c8d4 100644 --- a/tests/test_intersection.py +++ b/tests/test_intersection.py @@ -35,19 +35,19 @@ def test_intersection_eq(): def test_intersection_eq_hash(): assert hash(value) == hash(value) - assert hash(value) != other + assert hash(value) != other # type: ignore[comparison-overlap] def test_intersection_instancecheck(): - assert isinstance(C(), value) # type: ignore[arg-type] - assert not isinstance(A(), value) # type: ignore[arg-type] - assert not isinstance(B(), value) # type: ignore[arg-type] + assert isinstance(C(), value) # type: ignore[arg-type, misc] + assert not isinstance(A(), value) # type: ignore[arg-type, misc] + assert not isinstance(B(), value) # type: ignore[arg-type, misc] def test_intersection_subclasscheck(): - assert issubclass(C, value) # type: ignore[arg-type] - assert not issubclass(A, value) # type: ignore[arg-type] - assert not issubclass(B, value) # type: ignore[arg-type] + assert issubclass(C, value) # type: ignore[arg-type, misc] + assert not issubclass(A, value) # type: ignore[arg-type, misc] + assert not issubclass(B, value) # type: ignore[arg-type, misc] def test_intersection_reduce(): diff --git a/tests/test_is_subform.py b/tests/test_is_subform.py index 7f8bf17..a41c841 100644 --- a/tests/test_is_subform.py +++ b/tests/test_is_subform.py @@ -21,14 +21,11 @@ def test_union_first_arg(): def test_old_union(): # TODO: fix the mypy error # noqa: TD003 - assert not issubform(Union[int, str], int) # type: ignore[arg-type] - assert issubform(Union[int, str], object) # type: ignore[arg-type] - assert issubform( - Union[int, str], # type: ignore[arg-type] - Union[str, int], # type: ignore[arg-type] - ) + assert not issubform(Union[int, str], int) + assert issubform(Union[int, str], object) + assert issubform(Union[int, str], Union[str, int]) if sys.version_info >= (3, 10): assert issubform( - Union[int, str], # type: ignore[arg-type] + Union[int, str], int | str, # type: ignore[unused-ignore, arg-type] ) diff --git a/tests/test_reified_generics/test_reified_generic.py b/tests/test_reified_generics/test_reified_generic.py index fe921c1..c489d76 100644 --- a/tests/test_reified_generics/test_reified_generic.py +++ b/tests/test_reified_generics/test_reified_generic.py @@ -79,9 +79,7 @@ class SubASpecified(A[int]): def test_none_type(): - # TODO: is this mypy error correct? - # https://github.com/KotlinIsland/basedtyping/issues/74 - assert Reified[None, None].__reified_generics__ == (NoneType, NoneType) # type:ignore[comparison-overlap] + assert Reified[None, None].__reified_generics__ == (NoneType, NoneType) if TYPE_CHECKING: diff --git a/tests/test_runtime_only/test_literal_type.py b/tests/test_runtime_only/test_literal_type.py index 7d8effd..eb9f6e9 100644 --- a/tests/test_runtime_only/test_literal_type.py +++ b/tests/test_runtime_only/test_literal_type.py @@ -1,22 +1,17 @@ from __future__ import annotations -import sys from typing import Union -import pytest -if sys.version_info >= (3, 9): # prevent mypy errors +def test_literal_type_positive(): + from typing import Literal - @pytest.mark.skipif(sys.version_info < (3, 9), reason="need 3.9 for LiteralType") - def test_literal_type_positive(): - from typing import Literal + from basedtyping.runtime_only import LiteralType - from basedtyping.runtime_only import LiteralType + assert isinstance(Literal[1, 2], LiteralType) - assert isinstance(Literal[1, 2], LiteralType) - @pytest.mark.skipif(sys.version_info < (3, 9), reason="need 3.9 for LiteralType") - def test_literal_type_negative(): - from basedtyping.runtime_only import LiteralType +def test_literal_type_negative(): + from basedtyping.runtime_only import LiteralType - assert not isinstance(Union[int, str], LiteralType) + assert not isinstance(Union[int, str], LiteralType) diff --git a/tests/test_transformer.py b/tests/test_transformer.py index a3c6117..ae94638 100644 --- a/tests/test_transformer.py +++ b/tests/test_transformer.py @@ -1,34 +1,57 @@ +from __future__ import annotations + +import sys from enum import Enum -from typing import Tuple +from types import FunctionType # noqa: F401 +from typing import Dict, List, Tuple, cast +from unittest import skipIf + +from pytest import raises +from typing_extensions import Annotated, Callable, Literal, TypeGuard, TypeIs, Union -from transformer import eval_type_based -from typing_extensions import Callable, Literal, TypeIs, Union, TypeGuard -from types import FunctionType from basedtyping import ForwardRef, Intersection +from basedtyping.transformer import eval_type_based -# ruff: noqa: PYI030, PYI030 +# ruff: noqa: PYI030 the unions of literals are an artifact of the implementation, they have no bearing on anything practical -def validate(value, expected, *, string_literals=False): +def validate(value: str, expected: object, *, string_literals=False): assert ( - eval_type_based(ForwardRef(value), globalns=globals(), string_literals=string_literals) + eval_type_based( + ForwardRef(value), + globalns=cast(Dict[str, object], globals()), + string_literals=string_literals, + ) == expected ) +@skipIf(sys.version_info <= (3, 10), "unsupported") # type: ignore[no-any-expr] def test_literal(): validate("1 | 2", Union[Literal[1], Literal[2]]) +def test_literal_union(): + validate("Union[1, 2]", Union[Literal[1], Literal[2]]) + + def test_literal_literal(): validate("Literal[1]", Literal[1]) -def test_literal_str(): - validate("'int'", int) - validate("Literal['int']", Literal["int"]) - validate("'int'", Literal["int"], string_literals=True) - validate("Literal['int']", Literal["int"], string_literals=True) +def test_literal_nested(): + validate("(1, 2)", Tuple[Literal[1], Literal[2]]) + validate("List[(1, 2),]", List[Tuple[Literal[1], Literal[2]]]) + + +def test_literal_str_forwardref(): + validate("'1'", Literal[1]) + validate("Literal['1']", Literal["1"]) + + +def test_literal_str_literal(): + validate("'1'", Literal["1"], string_literals=True) + validate("Literal['1']", Literal["1"], string_literals=True) class E(Enum): @@ -36,14 +59,23 @@ class E(Enum): b = 2 +@skipIf(sys.version_info <= (3, 10), "unsupported") # type: ignore[no-any-expr] def test_literal_enum(): validate("E.a | E.b", Union[Literal[E.a], Literal[E.b]]) +def test_literal_enum_union(): + validate("Union[E.a, E.b]", Union[Literal[E.a], Literal[E.b]]) + + def test_tuple(): validate("(int, str)", Tuple[int, str]) +def test_tuple_nested(): + validate("List[(int, str),]", List[Tuple[int, str]]) + + def test_typeguard(): validate("x is 1", TypeIs[Literal[1]]) @@ -61,6 +93,13 @@ def test_function(): validate("FunctionType[[str], int]", Callable[[str], int]) +def_ = int + + +def test_adversarial_function(): + validate("Union[def_, '() -> int']", Union[def_, Callable[[], int]]) + + def test_functiontype(): validate("FunctionType[[str], int]", Callable[[str], int]) @@ -69,5 +108,10 @@ def test_intersection(): validate("int & str", Intersection[int, str]) -def test_nested(): - validate("(1, 2)", Tuple[Literal[1], Literal[2]]) +def test_annotated(): + validate("Annotated[1, 1]", Annotated[Literal[1], 1]) + + +def test_syntax_error(): + with raises(SyntaxError): + validate("among us", None)