Skip to content

Commit

Permalink
add FunctionType and rework TypeVars
Browse files Browse the repository at this point in the history
  • Loading branch information
KotlinIsland committed Oct 31, 2024
1 parent 8f75555 commit b7ae803
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 35 deletions.
59 changes: 35 additions & 24 deletions basedtyping/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,6 +26,7 @@
_remove_dups_flatten,
_SpecialForm,
_tp_cache,
Mapping,
cast,
)

Expand All @@ -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"
"""

Expand All @@ -50,7 +53,10 @@
from typing import _collect_type_vars as _collect_parameters

__all__ = (
"Function",
"AnyCallable",
"FunctionType",
"TCallable",
"TFunction",
"T",
"in_T",
"out_T",
Expand Down Expand Up @@ -106,27 +112,35 @@ 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")
in_T = TypeVar("in_T", contravariant=True)
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.
Expand Down Expand Up @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
4 changes: 2 additions & 2 deletions tests/test_function_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...)
Expand Down
12 changes: 6 additions & 6 deletions tests/test_intersection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
1 change: 0 additions & 1 deletion tests/test_is_subform.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
10 changes: 10 additions & 0 deletions tests/test_typevars.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit b7ae803

Please sign in to comment.