From b7ae8030766cc1080f476e448a9829675009704d Mon Sep 17 00:00:00 2001 From: KotlinIsland <65446343+kotlinisland@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:25:33 +1000 Subject: [PATCH] add `FunctionType` and rework `TypeVars` --- basedtyping/__init__.py | 59 ++++++++++++++++++++++--------------- pyproject.toml | 4 +-- tests/test_function_type.py | 4 +-- tests/test_intersection.py | 12 ++++---- tests/test_is_subform.py | 1 - tests/test_typevars.py | 10 +++++++ 6 files changed, 55 insertions(+), 35 deletions(-) create mode 100644 tests/test_typevars.py diff --git a/basedtyping/__init__.py b/basedtyping/__init__.py index 6ef6adf..c1d31b3 100644 --- a/basedtyping/__init__.py +++ b/basedtyping/__init__.py @@ -6,15 +6,16 @@ import ast import sys +import types import typing import warnings -from types import FunctionType from typing import ( # type: ignore[attr-defined] TYPE_CHECKING, Any, Callable, Final, Generic, + Literal, NoReturn, Sequence, Tuple, @@ -25,6 +26,7 @@ _remove_dups_flatten, _SpecialForm, _tp_cache, + Mapping, cast, ) @@ -37,9 +39,10 @@ # 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 +BASEDPYRIGHT_TYPE_CHECKING: Final = False +"""special constants, atr 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 +you may have to configure your type checker to consider this variable "always false" """ @@ -50,7 +53,10 @@ from typing import _collect_type_vars as _collect_parameters __all__ = ( - "Function", + "AnyCallable", + "FunctionType", + "TCallable", + "TFunction", "T", "in_T", "out_T", @@ -106,18 +112,6 @@ def __rand__(self, other: object) -> object: return Intersection[other, self] -if TYPE_CHECKING: - Function = Callable[..., object] # type: ignore[no-any-explicit] - """Any ``Callable``. useful when using mypy with ``disallow-any-explicit`` - due to https://github.com/python/mypy/issues/9496 - - Cannot actually be called unless it's narrowed, so it should only really be used as - a bound in a ``TypeVar``. - """ -else: - # for isinstance checks - Function = Callable - # Unlike the generics in other modules, these are meant to be imported to save you # from the boilerplate T = TypeVar("T") @@ -125,8 +119,28 @@ def __rand__(self, other: object) -> object: out_T = TypeVar("out_T", covariant=True) Ts = TypeVarTuple("Ts") P = ParamSpec("P") -Fn = TypeVar("Fn", bound=Function) +AnyCallable = Callable[..., object] # type: ignore[no-any-explicit] +"""Any ``Callable``. useful when using mypy with ``disallow-any-explicit`` +due to https://github.com/python/mypy/issues/9496 + +Cannot actually be called unless it's narrowed, so it should only really be used as +a bound in a ``TypeVar``. +""" + + +if not BASEDMYPY_TYPE_CHECKING and TYPE_CHECKING: + FunctionType: TypeAlias = Callable[P, T] +else: + # TODO: BasedSpecialGenericAlias + FunctionType: _SpecialForm = typing._CallableType(types.FunctionType, 2) # type: ignore[attr-defined] + +AnyFunction = FunctionType[..., object] # type: ignore[no-any-explicit] + +TCallable = TypeVar("TCallable", bound=AnyCallable) +TFunction = TypeVar("TFunction", bound=AnyFunction) +Fn = TypeVar("Fn", bound=Callable[..., object]) # type: ignore[no-any-explicit] +"""this is deprecated, use `TCallable` or `TFunction` instead""" def _type_convert(arg: object) -> object: """For converting None to type(None), and strings to ForwardRef. @@ -583,8 +597,6 @@ 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]: """Asserts that a ``Callable`` is a ``FunctionType`` and returns it @@ -596,10 +608,9 @@ def deco(fn: Callable[[], None]) -> Callable[[], None]: ... @deco def foo(): ... """ - if not isinstance(fn, FunctionType): + if not isinstance(fn, types.FunctionType): raise TypeError(f"{fn} is not a FunctionType") - # https://github.com/KotlinIsland/basedmypy/issues/745 - return cast("FunctionType[P, T]", fn) + return cast(FunctionType[P, T], fn) class ForwardRef(typing.ForwardRef, _root=True): # type: ignore[call-arg,misc] @@ -681,10 +692,10 @@ def _evaluate( def _evaluate( self, globalns: dict[str, object] | None, - localns: dict[str, object] | None, + localns: Mapping[str, object] | None, recursive_guard: frozenset[str], ) -> object | None: - return transformer._eval_direct(self, globalns, localns) + return transformer._eval_direct(self, globalns, localns if localns is None else dict(localns)) def _type_check(arg: object, msg: str) -> object: diff --git a/pyproject.toml b/pyproject.toml index bdc315b..28616b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,14 +5,14 @@ authors = [ ] description = "Utilities for basedmypy" name = "basedtyping" -version = "0.1.5" +version = "0.1.6" [tool.poetry.dependencies] python = "^3.9" typing_extensions = "^4.12.2" [tool.poetry.group.dev.dependencies] -basedmypy = "^2" +basedmypy = "^2.7" pytest = "^8" ruff = "~0.2.1" diff --git a/tests/test_function_type.py b/tests/test_function_type.py index 6b7cdc3..704ce08 100644 --- a/tests/test_function_type.py +++ b/tests/test_function_type.py @@ -5,10 +5,10 @@ if TYPE_CHECKING: # these are just type-time tests, not real life pytest tests. they are only run by mypy - from basedtyping import Function + from basedtyping import AnyCallable from basedtyping.typetime_only import assert_type - assert_function = assert_type[Function] + assert_function = assert_type[AnyCallable] def test_lambda_type(): assert_function(lambda: ...) diff --git a/tests/test_intersection.py b/tests/test_intersection.py index 3f8c8d4..fca29f7 100644 --- a/tests/test_intersection.py +++ b/tests/test_intersection.py @@ -39,15 +39,15 @@ def test_intersection_eq_hash(): def test_intersection_instancecheck(): - 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] + assert isinstance(C(), value) # type: ignore[misc, arg-type, misc] + assert not isinstance(A(), value) # type: ignore[misc, arg-type, misc] + assert not isinstance(B(), value) # type: ignore[misc, arg-type, misc] def test_intersection_subclasscheck(): - 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] + assert issubclass(C, value) # type: ignore[misc, arg-type, misc] + assert not issubclass(A, value) # type: ignore[misc, arg-type, misc] + assert not issubclass(B, value) # type: ignore[misc, arg-type, misc] def test_intersection_reduce(): diff --git a/tests/test_is_subform.py b/tests/test_is_subform.py index a41c841..511fffe 100644 --- a/tests/test_is_subform.py +++ b/tests/test_is_subform.py @@ -20,7 +20,6 @@ def test_union_first_arg(): def test_old_union(): - # TODO: fix the mypy error # noqa: TD003 assert not issubform(Union[int, str], int) assert issubform(Union[int, str], object) assert issubform(Union[int, str], Union[str, int]) diff --git a/tests/test_typevars.py b/tests/test_typevars.py new file mode 100644 index 0000000..2f7473b --- /dev/null +++ b/tests/test_typevars.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from basedtyping import AnyCallable + + +def test_any_callable(): + a: AnyCallable[[int], int] = lambda n: n # noqa: E731, F841