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/basedtyping/__init__.py b/basedtyping/__init__.py index 1dd578d..6ef6adf 100644 --- a/basedtyping/__init__.py +++ b/basedtyping/__init__.py @@ -4,14 +4,16 @@ from __future__ import annotations +import ast 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, @@ -23,15 +25,24 @@ _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 +# TODO: `Final[Literal[False]]` but basedmypy will whinge on usages +# https://github.com/KotlinIsland/basedmypy/issues/782 +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 @@ -55,6 +66,8 @@ "Intersection", "TypeForm", "as_functiontype", + "ForwardRef", + "BASEDMYPY_TYPE_CHECKING", ) if TYPE_CHECKING: @@ -66,13 +79,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 @@ -82,12 +97,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: @@ -247,6 +256,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 @@ -267,6 +277,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 @@ -275,6 +286,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. @@ -403,6 +415,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__() @@ -457,11 +470,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 @@ -473,26 +482,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__)) @@ -502,63 +509,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] - 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] - -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,) @@ -580,7 +585,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: @@ -591,6 +596,120 @@ 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] + """ + Like `typing.ForwardRef`, but lets older Python versions use newer typing features. + Specifically, when evaluated, 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. + """ + + # 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. + arg_to_compile = ( + f"({arg},)[0]" # E.g. (*Ts,)[0] or (*tuple[int, int],)[0] + if arg.startswith("*") + else arg + ) + try: + 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: + 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 + self.__forward_evaluated__ = False + self.__forward_value__ = None + self.__forward_is_argument__ = is_argument + self.__forward_is_class__ = is_class + self.__forward_module__ = module + + 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 new file mode 100644 index 0000000..473ea4a --- /dev/null +++ b/basedtyping/transformer.py @@ -0,0 +1,273 @@ +"""utilities to create standard compatible annotations""" + +from __future__ import annotations + +import ast +import sys +import types +import typing +from contextlib import contextmanager +from dataclasses import dataclass +from enum import Enum +from functools import partial +from typing import cast + +import typing_extensions +from typing_extensions import override + +import basedtyping + + +@dataclass +class EvalFailedError(TypeError): + """Raised when `CringeTransformer.eval_type` fails""" + + message: str + ref: typing.ForwardRef + transformer: CringeTransformer + + +# ruff: noqa: S101 erm, i wanted to use assert TODO: do something better +class CringeTransformer(ast.NodeTransformer): + """Transforms `1 | 2` into `Literal[1] | Literal[2]` etc""" + + def __init__( + self, + globalns: dict[str, object] | None, + localns: dict[str, object] | None, + *, + string_literals: bool, + ): + self.string_literals = string_literals + + # This logic for handling Nones is copied from typing.ForwardRef._evaluate + if globalns is None and localns is None: + globalns = localns = {} + elif globalns is None: + assert localns is not None + globalns = localns + elif localns is None: + assert globalns is not None + localns = globalns + + 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 + + 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.FunctionType | ast.Expression | ast.expr, + *, + original_ref: typing.ForwardRef | None = None, + ) -> object: + if isinstance(node, ast.expr): + node = ast.copy_location(ast.Expression(node), node) + ref = typing.ForwardRef(ast.unparse(node)) + if original_ref: + for attr in ("is_argument", " is_class", "module"): + attr = f"__forward_{attr}__" + if hasattr(original_ref, attr): + setattr(ref, attr, cast(object, getattr(original_ref, attr))) + if not isinstance(node, ast.FunctionType): + ref.__forward_code__ = compile(node, "", "eval") + try: + 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, 13): + return typing._eval_type( # type: ignore[attr-defined] + type_, self.globalns, self.localns, type_params=() + ) + else: # noqa: RET505 mypy prefers it in different branches TODO: raise an issue + return typing._eval_type( # type: ignore[attr-defined] + type_, self.globalns, self.localns + ) + except TypeError as e: + raise EvalFailedError(str(e), ref, self) from e + + 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) -> 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) -> ast.Subscript: + return self.subscript(self._typing("Literal"), value) + + 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, *, value=True) -> typing.Iterator[None]: + implicit_tuple = self._implicit_tuple + 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(): + 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: + slice2_ = node.slice + node = self.subscript(self._typing("Callable"), slice2_) + return node + + @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): + assert isinstance(node, (ast.Name, ast.Attribute)) + return self._literal(node) + return node + + @override + def visit_Name(self, node: ast.Name) -> ast.AST: + name_type = self.eval_type(node) + if isinstance(name_type, Enum): + return self._literal(node) + return node + + @override + def visit_Constant(self, node: ast.Constant) -> ast.AST: + 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: + with self.implicit_tuple(value=False): + result = self.generic_visit(node) + if not self._implicit_tuple: + 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): + 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) + and len(node.body.comparators) == 1 + and isinstance(node.body.ops[0], ast.Is) + ): + node.body = self.subscript( + 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: # noqa: N802 https://github.com/KotlinIsland/basedmypy/issues/763 + node = self.generic_visit(node) + 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, 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") + + tree = self.visit(tree) + assert isinstance(tree, ast.Expression) + return tree + + +def eval_type_based( + value: object, + 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. + """ + if not isinstance(value, typing.ForwardRef): + return value + transformer = CringeTransformer(globalns, localns, string_literals=string_literals) + 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..bdc315b 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,9 @@ 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"] +always_false = ["BASEDMYPY_TYPE_CHECKING"] [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..cf951d7 --- /dev/null +++ b/tests/test_basedmypy_typechecking.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from basedtyping import BASEDMYPY_TYPE_CHECKING + + +def _(): + """type test""" + 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 new file mode 100644 index 0000000..8072e4a --- /dev/null +++ b/tests/test_transformer.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +import sys +from enum import Enum +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 basedtyping import ForwardRef, Intersection +from basedtyping.transformer import eval_type_based + +# ruff: noqa: PYI030 the unions of literals are an artifact of the implementation, they have no bearing on anything practical + + +def validate(value: str, expected: object, *, string_literals=False): + assert ( + 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_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): + a = 1 + 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]]) + + +def test_typeguard_asymmetric(): + validate("x is 1 if True else False", TypeGuard[Literal[1]]) + + +def test_callable(): + validate("(str) -> int", Callable[[str], int]) + + +def test_function(): + validate("def (str) -> int", Callable[[str], int]) + 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]) + + +def test_intersection(): + validate("int & str", Intersection[int, str]) + + +def test_annotated(): + validate("Annotated[1, 1]", Annotated[Literal[1], 1]) + + +def test_syntax_error(): + with raises(SyntaxError): + validate("among us", None) + + +def test_unsupported(): + with raises(TypeError): + validate("int + str", None)