diff --git a/pyproject.toml b/pyproject.toml index edb2e3d..5e1a4b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ include = ["typemap", "typemap.*", "typemap_extensions"] test = [ "pytest>=7.0", "ruff", - "mypy @ git+https://github.com/msullivan/mypy-typemap@f127ae7a0b79b0d6b3fee9c82e75e62a12ac39e3", + "mypy @ git+https://github.com/msullivan/mypy-typemap@711205f297cfe94c998a39b6b252add796378cb1", ] [tool.uv] diff --git a/tests/test_dataclass_like.py b/tests/test_dataclass_like.py index 9b108c9..29a0774 100644 --- a/tests/test_dataclass_like.py +++ b/tests/test_dataclass_like.py @@ -69,7 +69,7 @@ def _check_hero_init() -> None: p.name, p.type, # All arguments are keyword-only - Literal["keyword"], + Literal[typing.ParamKind.KEYWORD_ONLY], # GetDefault is Never when there's no default, so use it # directly as D. GetDefault[p.init], diff --git a/tests/test_eval_call_with_types.py b/tests/test_eval_call_with_types.py index 1c953b8..55d4c8c 100644 --- a/tests/test_eval_call_with_types.py +++ b/tests/test_eval_call_with_types.py @@ -12,6 +12,7 @@ Iter, Members, Param, + ParamKind, Params, ) @@ -30,7 +31,10 @@ def test_eval_call_with_types_callable_02(): def test_eval_call_with_types_callable_03(): res = eval_call_with_types( - Callable[Params[Param[Literal["x"], int, Literal["keyword"]]], int], + Callable[ + Params[Param[Literal["x"], int, Literal[ParamKind.KEYWORD_ONLY]]], + int, + ], x=int, ) assert res is int @@ -75,7 +79,7 @@ class C: ... Callable[ Params[ Param[Literal["self"], Self], - Param[Literal["x"], int, Literal["keyword"]], + Param[Literal["x"], int, Literal[ParamKind.KEYWORD_ONLY]], ], int, ], diff --git a/tests/test_fastapilike_1.py b/tests/test_fastapilike_1.py index 04a2a3f..e7c391e 100644 --- a/tests/test_fastapilike_1.py +++ b/tests/test_fastapilike_1.py @@ -22,6 +22,7 @@ Member, Members, Param, + ParamKind, Params, ) @@ -57,7 +58,7 @@ class _Default: Param[ p.name, DropAnnotations[p.type], - Literal["keyword"], + Literal[ParamKind.KEYWORD_ONLY], DropAnnotations[p.type] if IsAssignable[ Literal[PropQuals.HAS_DEFAULT], diff --git a/tests/test_fastapilike_2.py b/tests/test_fastapilike_2.py index a22b488..8a7ccec 100644 --- a/tests/test_fastapilike_2.py +++ b/tests/test_fastapilike_2.py @@ -149,7 +149,7 @@ class Field[T: FieldArgs](typing.InitField[T]): p.name, p.type, # All arguments are keyword-only - Literal["keyword"], + Literal[typing.ParamKind.KEYWORD_ONLY], # GetDefault is Never when there's no default, so use it # directly as D. GetDefault[p.init], diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 667b8ce..94f8b41 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -385,9 +385,9 @@ def test_type_members_func_1(): str(typ) == "\ typing.Callable[\ -typemap.typing.Params[typemap.typing.Param[typing.Literal['self'], tests.test_type_dir.Base[int], typing.Literal['positional_or_keyword'], typing.Never], \ -typemap.typing.Param[typing.Literal['a'], int | None, typing.Literal['positional_or_keyword'], typing.Never], \ -typemap.typing.Param[typing.Literal['b'], int, typing.Literal['keyword'], typing.Literal[0]]], \ +typemap.typing.Params[typemap.typing.Param[typing.Literal['self'], tests.test_type_dir.Base[int], typing.Literal[], typing.Never], \ +typemap.typing.Param[typing.Literal['a'], int | None, typing.Literal[], typing.Never], \ +typemap.typing.Param[typing.Literal['b'], int, typing.Literal[], typing.Literal[0]]], \ dict[str, int]]" ) @@ -405,7 +405,7 @@ def test_type_members_func_2(): assert ( str(typ) == "\ -classmethod[tests.test_type_dir.Base[int], typemap.typing.Params[typemap.typing.Param[typing.Literal['a'], int | None, typing.Literal['positional_or_keyword'], typing.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Literal['positional_or_keyword'], typing.Never]], dict[str, int]]" +classmethod[tests.test_type_dir.Base[int], typemap.typing.Params[typemap.typing.Param[typing.Literal['a'], int | None, typing.Literal[], typing.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Literal[], typing.Never]], dict[str, int]]" ) @@ -424,7 +424,7 @@ def test_type_members_func_3(): ) assert ( str(evaled) - == "staticmethod[typemap.typing.Params[typemap.typing.Param[typing.Literal['a'], int | typing.Literal['gotcha!'] | Z | None, typing.Literal['positional_or_keyword'], typing.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Literal['positional_or_keyword'], typing.Never]], dict[str, int | Z]]" + == "staticmethod[typemap.typing.Params[typemap.typing.Param[typing.Literal['a'], int | typing.Literal['gotcha!'] | Z | None, typing.Literal[], typing.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Literal[], typing.Never]], dict[str, int | Z]]" ) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 5f6a050..7721bc4 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -47,6 +47,7 @@ NewProtocol, Overloaded, Param, + ParamKind, Params, Slice, SpecialFormEllipsis, @@ -155,13 +156,18 @@ def test_eval_types_4(): d = eval_typing( Callable[ [ - Param[Literal["a"], int, Literal["positional"]], + Param[Literal["a"], int, Literal[ParamKind.POSITIONAL_ONLY]], Param[Literal["b"], int], - Param[Literal["c"], int, Literal["positional_or_keyword"], int], - Param[None, int, Literal["*"]], - Param[Literal["d"], int, Literal["keyword"]], - Param[Literal["e"], int, Literal["keyword"], int], - Param[None, int, Literal["**"]], + Param[ + Literal["c"], + int, + Literal[ParamKind.POSITIONAL_OR_KEYWORD], + int, + ], + Param[None, int, Literal[ParamKind.VAR_POSITIONAL]], + Param[Literal["d"], int, Literal[ParamKind.KEYWORD_ONLY]], + Param[Literal["e"], int, Literal[ParamKind.KEYWORD_ONLY], int], + Param[None, int, Literal[ParamKind.VAR_KEYWORD]], ], int, ] @@ -170,13 +176,18 @@ def test_eval_types_4(): d == Callable[ [ - Param[Literal["a"], int, Literal["positional"]], + Param[Literal["a"], int, Literal[ParamKind.POSITIONAL_ONLY]], Param[Literal["b"], int], - Param[Literal["c"], int, Literal["positional_or_keyword"], int], - Param[None, int, Literal["*"]], - Param[Literal["d"], int, Literal["keyword"]], - Param[Literal["e"], int, Literal["keyword"], int], - Param[None, int, Literal["**"]], + Param[ + Literal["c"], + int, + Literal[ParamKind.POSITIONAL_OR_KEYWORD], + int, + ], + Param[None, int, Literal[ParamKind.VAR_POSITIONAL]], + Param[Literal["d"], int, Literal[ParamKind.KEYWORD_ONLY]], + Param[Literal["e"], int, Literal[ParamKind.KEYWORD_ONLY], int], + Param[None, int, Literal[ParamKind.VAR_KEYWORD]], ], int, ] @@ -765,8 +776,8 @@ def test_eval_getarg_callable_old(): assert ( args == Params[ - Param[Literal[None], Any, Literal["*"]], - Param[Literal[None], Any, Literal["**"]], + Param[Literal[None], Any, Literal[ParamKind.VAR_POSITIONAL]], + Param[Literal[None], Any, Literal[ParamKind.VAR_KEYWORD]], ] ) @@ -775,8 +786,8 @@ def test_eval_getarg_callable_old(): assert ( args == Params[ - Param[Literal[None], Any, Literal["*"]], - Param[Literal[None], Any, Literal["**"]], + Param[Literal[None], Any, Literal[ParamKind.VAR_POSITIONAL]], + Param[Literal[None], Any, Literal[ParamKind.VAR_KEYWORD]], ] ) @@ -804,8 +815,8 @@ def test_eval_getarg_callable_01(): assert ( args == Params[ - Param[Literal[None], Any, Literal["*"]], - Param[Literal[None], Any, Literal["**"]], + Param[Literal[None], Any, Literal[ParamKind.VAR_POSITIONAL]], + Param[Literal[None], Any, Literal[ParamKind.VAR_KEYWORD]], ] ) @@ -814,8 +825,8 @@ def test_eval_getarg_callable_01(): assert ( args == Params[ - Param[Literal[None], Any, Literal["*"]], - Param[Literal[None], Any, Literal["**"]], + Param[Literal[None], Any, Literal[ParamKind.VAR_POSITIONAL]], + Param[Literal[None], Any, Literal[ParamKind.VAR_KEYWORD]], ] ) @@ -839,9 +850,9 @@ def test_eval_getarg_callable_02(): # Params wrapped f = Callable[ [ - Param[Literal[None], T, Literal["positional"]], + Param[Literal[None], T, Literal[ParamKind.POSITIONAL_ONLY]], Param[Literal["y"], T], - Param[Literal["z"], T, Literal["keyword"]], + Param[Literal["z"], T, Literal[ParamKind.KEYWORD_ONLY]], ], T, ] @@ -885,10 +896,10 @@ def f(self, x: int, /, y: int, *, z: int) -> int: ... assert ( t == Params[ - Param[Literal["self"], C, Literal["positional"]], - Param[Literal["x"], int, Literal["positional"]], + Param[Literal["self"], C, Literal[ParamKind.POSITIONAL_ONLY]], + Param[Literal["x"], int, Literal[ParamKind.POSITIONAL_ONLY]], Param[Literal["y"], int], - Param[Literal["z"], int, Literal["keyword"]], + Param[Literal["z"], int, Literal[ParamKind.KEYWORD_ONLY]], ] ) t = eval_typing(GetArg[f, Callable, Literal[1]]) @@ -899,10 +910,10 @@ def f(self, x: int, /, y: int, *, z: int) -> int: ... assert ( t == Params[ - Param[Literal["self"], Self, Literal["positional"]], - Param[Literal["x"], int, Literal["positional"]], + Param[Literal["self"], Self, Literal[ParamKind.POSITIONAL_ONLY]], + Param[Literal["x"], int, Literal[ParamKind.POSITIONAL_ONLY]], Param[Literal["y"], int], - Param[Literal["z"], int, Literal["keyword"]], + Param[Literal["z"], int, Literal[ParamKind.KEYWORD_ONLY]], ] ) t = eval_typing(GetArg[f, Callable, Literal[1]]) @@ -922,9 +933,9 @@ def f(cls, x: int, /, y: int, *, z: int) -> int: ... assert ( t == Params[ - Param[Literal["x"], int, Literal["positional"]], + Param[Literal["x"], int, Literal[ParamKind.POSITIONAL_ONLY]], Param[Literal["y"], int], - Param[Literal["z"], int, Literal["keyword"]], + Param[Literal["z"], int, Literal[ParamKind.KEYWORD_ONLY]], ] ) t = eval_typing(GetArg[f, classmethod, Literal[2]]) @@ -936,9 +947,9 @@ def f(cls, x: int, /, y: int, *, z: int) -> int: ... assert ( t == Params[ - Param[Literal["x"], int, Literal["positional"]], + Param[Literal["x"], int, Literal[ParamKind.POSITIONAL_ONLY]], Param[Literal["y"], int], - Param[Literal["z"], int, Literal["keyword"]], + Param[Literal["z"], int, Literal[ParamKind.KEYWORD_ONLY]], ] ) t = eval_typing(GetArg[f, classmethod, Literal[2]]) @@ -956,9 +967,9 @@ def f(x: int, /, y: int, *, z: int) -> int: ... assert ( t == Params[ - Param[Literal["x"], int, Literal["positional"]], + Param[Literal["x"], int, Literal[ParamKind.POSITIONAL_ONLY]], Param[Literal["y"], int], - Param[Literal["z"], int, Literal["keyword"]], + Param[Literal["z"], int, Literal[ParamKind.KEYWORD_ONLY]], ] ) t = eval_typing(GetArg[f, staticmethod, Literal[1]]) @@ -969,9 +980,9 @@ def f(x: int, /, y: int, *, z: int) -> int: ... assert ( t == Params[ - Param[Literal["x"], int, Literal["positional"]], + Param[Literal["x"], int, Literal[ParamKind.POSITIONAL_ONLY]], Param[Literal["y"], int], - Param[Literal["z"], int, Literal["keyword"]], + Param[Literal["z"], int, Literal[ParamKind.KEYWORD_ONLY]], ] ) t = eval_typing(GetArg[f, staticmethod, Literal[1]]) @@ -1827,11 +1838,13 @@ def test_callable_to_signature_01(): Params[ Param[None, int], Param[Literal["b"], int], - Param[Literal["c"], int, Literal["positional_or_keyword"], int], - Param[None, int, Literal["*"]], - Param[Literal["d"], int, Literal["keyword"]], - Param[Literal["e"], int, Literal["keyword"], int], - Param[None, int, Literal["**"]], + Param[ + Literal["c"], int, Literal[ParamKind.POSITIONAL_OR_KEYWORD], int + ], + Param[None, int, Literal[ParamKind.VAR_POSITIONAL]], + Param[Literal["d"], int, Literal[ParamKind.KEYWORD_ONLY]], + Param[Literal["e"], int, Literal[ParamKind.KEYWORD_ONLY], int], + Param[None, int, Literal[ParamKind.VAR_KEYWORD]], ], int, ] @@ -1850,10 +1863,16 @@ def test_callable_to_signature_01(): def test_callable_to_signature_multi_kind_error(): from typemap.type_eval._eval_operators import _callable_type_to_signature - # Param can carry at most one kind; combining "positional" with - # "keyword" is nonsense and should be rejected. + # Param can carry at most one kind; combining POSITIONAL_ONLY with + # KEYWORD_ONLY is nonsense and should be rejected. callable_type = Callable[ - Params[Param[Literal["x"], int, Literal["positional", "keyword"]]], + Params[ + Param[ + Literal["x"], + int, + Literal[ParamKind.POSITIONAL_ONLY, ParamKind.KEYWORD_ONLY], + ] + ], int, ] with pytest.raises(TypeError, match="at most one"): @@ -1864,7 +1883,7 @@ def test_callable_to_signature_never_kind_error(): from typemap.type_eval._eval_operators import _callable_type_to_signature # Never is not a valid kind; the explicit "normal" kind is - # Literal["positional_or_keyword"]. + # Literal[ParamKind.POSITIONAL_OR_KEYWORD] (the default). callable_type = Callable[ Params[Param[Literal["x"], int, Never]], int, diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 2b45997..6d1f224 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -45,6 +45,7 @@ NewTypedDict, Overloaded, Param, + ParamKind, Params, RaiseError, Slice, @@ -494,13 +495,13 @@ def _eval_Bool(tp, *, ctx): ################################################################## -def _get_kind(kind_type) -> str | None: - # Extract the single kind from a Literal of one ParamKind value. +def _get_kind(kind_type) -> ParamKind | None: + # Extract the single ParamKind from a Literal of one value. # Multiple kinds, or Never, are an error. if kind_type is typing.Never: raise TypeError( "Param kind cannot be Never; " - "use Literal['positional_or_keyword'] for the default" + "use Literal[ParamKind.POSITIONAL_OR_KEYWORD] for the default" ) if not _typing_inspect.is_literal(kind_type): return None @@ -534,8 +535,16 @@ def _unwrap_params(param_types) -> list: if param_types is ...: return [ - Param[typing.Literal[None], typing.Any, typing.Literal["*"]], - Param[typing.Literal[None], typing.Any, typing.Literal["**"]], + Param[ + typing.Literal[None], + typing.Any, + typing.Literal[ParamKind.VAR_POSITIONAL], + ], + Param[ + typing.Literal[None], + typing.Any, + typing.Literal[ParamKind.VAR_KEYWORD], + ], ] if isinstance(param_types, (list, tuple)): @@ -588,7 +597,7 @@ def _callable_type_to_signature(callable_type: object) -> inspect.Signature: kind_type = ( param_args[2] if len(param_args) > 2 - else typing.Literal["positional_or_keyword"] + else typing.Literal[ParamKind.POSITIONAL_OR_KEYWORD] ) # Extract name from Literal[name] or None @@ -598,20 +607,20 @@ def _callable_type_to_signature(callable_type: object) -> inspect.Signature: # Determine parameter kind and default kind: inspect._ParameterKind - if param_kind == "**": + if param_kind is ParamKind.VAR_KEYWORD: kind = inspect.Parameter.VAR_KEYWORD name = name or "kwargs" - elif param_kind == "*": + elif param_kind is ParamKind.VAR_POSITIONAL: kind = inspect.Parameter.VAR_POSITIONAL name = name or "args" # XXX: not sure we need this saw_keyword_only = True - elif param_kind == "keyword": + elif param_kind is ParamKind.KEYWORD_ONLY: kind = inspect.Parameter.KEYWORD_ONLY saw_keyword_only = True - elif param_kind == "positional" or name is None: + elif param_kind is ParamKind.POSITIONAL_ONLY or name is None: kind = inspect.Parameter.POSITIONAL_ONLY - elif param_kind == "positional_or_keyword": + elif param_kind is ParamKind.POSITIONAL_OR_KEYWORD: kind = inspect.Parameter.POSITIONAL_OR_KEYWORD elif saw_keyword_only: kind = inspect.Parameter.KEYWORD_ONLY @@ -678,7 +687,10 @@ def _is_pos_only(param): args = typing.get_args(param) name, _, kind_type = args[0], args[1], args[2] kind = _get_kind(kind_type) - return kind == "positional" or (name is None and kind not in ("*", "**")) + return kind is ParamKind.POSITIONAL_ONLY or ( + name is None + and kind not in (ParamKind.VAR_POSITIONAL, ParamKind.VAR_KEYWORD) + ) def _callable_type_to_method(name, typ, ctx): @@ -706,9 +718,9 @@ def _callable_type_to_method(name, typ, ctx): # positional only argument. Annoying! has_pos_only = any(_is_pos_only(p) for p in param_list) kind = ( - typing.Literal["positional"] + typing.Literal[ParamKind.POSITIONAL_ONLY] if has_pos_only - else typing.Literal["positional_or_keyword"] + else typing.Literal[ParamKind.POSITIONAL_OR_KEYWORD] ) # Override the receiver type with type[Self]. if name == "__init_subclass__" and isinstance(cls, typing.TypeVar): @@ -779,17 +791,7 @@ def _ann(x): else: specified_receiver = ann - kinds = [] - if p.kind == inspect.Parameter.VAR_POSITIONAL: - kinds.append("*") - if p.kind == inspect.Parameter.VAR_KEYWORD: - kinds.append("**") - if p.kind == inspect.Parameter.KEYWORD_ONLY: - kinds.append("keyword") - if p.kind == inspect.Parameter.POSITIONAL_ONLY: - kinds.append("positional") - if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: - kinds.append("positional_or_keyword") + kind = ParamKind(p.kind.value) ann_type = _ann(ann) has_default = p.default is not empty if has_default: @@ -804,7 +806,7 @@ def _ann(x): Param[ typing.Literal[p.name], ann_type, - typing.Literal[*kinds] if kinds else typing.Never, + typing.Literal[kind], default_type, ] ) @@ -1085,8 +1087,16 @@ def _get_defaults(base_head): if base_head is collections.abc.Callable: return ( Params[ - Param[typing.Literal[None], typing.Any, typing.Literal["*"]], - Param[typing.Literal[None], typing.Any, typing.Literal["**"]], + Param[ + typing.Literal[None], + typing.Any, + typing.Literal[ParamKind.VAR_POSITIONAL], + ], + Param[ + typing.Literal[None], + typing.Any, + typing.Literal[ParamKind.VAR_KEYWORD], + ], ], typing.Any, ) diff --git a/typemap/typing.py b/typemap/typing.py index 9be92f2..ba106e5 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -1,6 +1,7 @@ # mypy: ignore-errors import contextvars +import enum import typing import types @@ -198,14 +199,21 @@ class Member[ type definer = D -ParamKind = Literal["*", "**", "keyword", "positional", "positional_or_keyword"] +class ParamKind(enum.IntEnum): + """Parameter kind qualifiers, modeled on inspect._ParameterKind.""" + + POSITIONAL_ONLY = 0 + POSITIONAL_OR_KEYWORD = 1 + VAR_POSITIONAL = 2 + KEYWORD_ONLY = 3 + VAR_KEYWORD = 4 @has_associated_types class Param[ N: str | None, T, - K: ParamKind = Literal["positional_or_keyword"], + K: ParamKind = Literal[ParamKind.POSITIONAL_OR_KEYWORD], D = typing.Never, ]: type name = N @@ -214,13 +222,17 @@ class Param[ type default = D -type PosParam[T] = Param[None, T, Literal["positional"]] -type PosDefaultParam[T] = Param[None, T, Literal["positional"], T] -type DefaultParam[N: str, T] = Param[N, T, Literal["positional_or_keyword"], T] -type NamedParam[N: str, T] = Param[N, T, Literal["keyword"]] -type NamedDefaultParam[N: str, T] = Param[N, T, Literal["keyword"], T] -type ArgsParam[T] = Param[None, T, Literal["*"]] -type KwargsParam[T] = Param[None, T, Literal["**"]] +type PosParam[T] = Param[None, T, Literal[ParamKind.POSITIONAL_ONLY]] +type PosDefaultParam[T] = Param[None, T, Literal[ParamKind.POSITIONAL_ONLY], T] +type DefaultParam[N: str, T] = Param[ + N, T, Literal[ParamKind.POSITIONAL_OR_KEYWORD], T +] +type NamedParam[N: str, T] = Param[N, T, Literal[ParamKind.KEYWORD_ONLY]] +type NamedDefaultParam[N: str, T] = Param[ + N, T, Literal[ParamKind.KEYWORD_ONLY], T +] +type ArgsParam[T] = Param[None, T, Literal[ParamKind.VAR_POSITIONAL]] +type KwargsParam[T] = Param[None, T, Literal[ParamKind.VAR_KEYWORD]] class Params: diff --git a/uv.lock b/uv.lock index 9ff2c16..52c99d0 100644 --- a/uv.lock +++ b/uv.lock @@ -60,8 +60,8 @@ wheels = [ [[package]] name = "mypy" -version = "1.20.0+dev.f127ae7a0b79b0d6b3fee9c82e75e62a12ac39e3" -source = { git = "https://github.com/msullivan/mypy-typemap?rev=f127ae7a0b79b0d6b3fee9c82e75e62a12ac39e3#f127ae7a0b79b0d6b3fee9c82e75e62a12ac39e3" } +version = "1.20.0+dev.711205f297cfe94c998a39b6b252add796378cb1" +source = { git = "https://github.com/msullivan/mypy-typemap?rev=711205f297cfe94c998a39b6b252add796378cb1#711205f297cfe94c998a39b6b252add796378cb1" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, @@ -172,7 +172,7 @@ test = [ [package.metadata.requires-dev] test = [ - { name = "mypy", git = "https://github.com/msullivan/mypy-typemap?rev=f127ae7a0b79b0d6b3fee9c82e75e62a12ac39e3" }, + { name = "mypy", git = "https://github.com/msullivan/mypy-typemap?rev=711205f297cfe94c998a39b6b252add796378cb1" }, { name = "pytest", specifier = ">=7.0" }, { name = "ruff" }, ]