Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions tests/test_dataclass_like.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,14 @@ def _check_hero_init() -> None:
p.name,
p.type,
# All arguments are keyword-only
Literal["keyword"],
# It takes a default if a default is specified in the class
Literal["keyword"]
if typing.IsAssignable[
p.type
if not typing.IsAssignable[
GetDefault[p.init],
Never,
]
else Literal["keyword", "default"],
else Never,
]
for p in typing.Iter[typing.Attrs[T]]
],
Expand Down
7 changes: 4 additions & 3 deletions tests/test_fastapilike_1.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import enum
import textwrap

from typing import Annotated, Callable, Literal, Union, Self
from typing import Annotated, Callable, Literal, Never, Union, Self

from typemap.type_eval import eval_typing
from typemap_extensions import (
Expand Down Expand Up @@ -57,12 +57,13 @@ class _Default:
Param[
p.name,
DropAnnotations[p.type],
Literal["keyword", "default"]
Literal["keyword"],
DropAnnotations[p.type]
if IsAssignable[
Literal[PropQuals.HAS_DEFAULT],
GetAnnotations[p.type],
]
else Literal["keyword"],
else Never,
]
for p in Iter[Attrs[T]]
],
Expand Down
7 changes: 4 additions & 3 deletions tests/test_fastapilike_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,14 @@ class Field[T: FieldArgs](typing.InitField[T]):
p.name,
p.type,
# All arguments are keyword-only
Literal["keyword"],
# It takes a default if a default is specified in the class
Literal["keyword"]
if typing.IsAssignable[
p.type
if not typing.IsAssignable[
GetDefault[p.init],
Never,
]
else Literal["keyword", "default"],
else Never,
]
for p in typing.Iter[typing.Attrs[T]]
],
Expand Down
15 changes: 14 additions & 1 deletion tests/test_mypy_proto.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
MYPY_SOURCE_DIR = pathlib.Path(_mypy_source).resolve() if _mypy_source else None


# Files that depend on the 4-arg Param shape, which the pinned
# mypy-typemap stub fork hasn't been updated for yet.
_XFAIL_PARAM_D = {"test_dataclass_like", "test_fastapilike_2"}


def _collect_mypy_test_files():
"""Collect test files that don't have # SKIP MYPY."""
tests_dir = pathlib.Path(__file__).parent
Expand All @@ -22,7 +27,15 @@ def _collect_mypy_test_files():
continue
text = path.read_text()
if "# SKIP MYPY" not in text:
yield pytest.param(path, id=path.stem)
marks = []
if path.stem in _XFAIL_PARAM_D:
marks.append(
pytest.mark.xfail(
reason="mypy-typemap stubs still have 3-arg Param",
strict=True,
)
)
yield pytest.param(path, id=path.stem, marks=marks)


@pytest.mark.parametrize("test_file", _collect_mypy_test_files())
Expand Down
13 changes: 6 additions & 7 deletions tests/test_type_dir.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ class Final:
fin: typing.Final[int]
x: tests.test_type_dir.Wrapper[int | None]
ordinary: str
def foo(self: Self, a: int | None, *, b: int = ...) -> dict[str, int]: ...
def foo(self: Self, a: int | None, *, b: int = 0) -> dict[str, int]: ...
def base[Z](self: Self, a: int | Z | None, b: ~K) -> dict[str, int | Z]: ...
@classmethod
def cbase(cls: type[typing.Self], a: int | None, b: ~K) -> dict[str, int]: ...
Expand Down Expand Up @@ -385,10 +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.Never], \
typemap.typing.Param[typing.Literal['a'], int | None, typing.Never], \
typemap.typing.Param[typing.Literal['b'], int, typing.Literal['keyword', \
'default']]], \
typemap.typing.Params[typemap.typing.Param[typing.Literal['self'], tests.test_type_dir.Base[int], typing.Never, typing.Never], \
typemap.typing.Param[typing.Literal['a'], int | None, typing.Never, typing.Never], \
typemap.typing.Param[typing.Literal['b'], int, typing.Literal['keyword'], typing.Literal[0]]], \
dict[str, int]]"
)

Expand All @@ -406,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.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Never]], dict[str, int]]"
classmethod[tests.test_type_dir.Base[int], typemap.typing.Params[typemap.typing.Param[typing.Literal['a'], int | None, typing.Never, typing.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Never, typing.Never]], dict[str, int]]"
)


Expand All @@ -425,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.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Never]], dict[str, int | Z]]"
== "staticmethod[typemap.typing.Params[typemap.typing.Param[typing.Literal['a'], int | typing.Literal['gotcha!'] | Z | None, typing.Never, typing.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Never, typing.Never]], dict[str, int | Z]]"
)


Expand Down
12 changes: 6 additions & 6 deletions tests/test_type_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,10 @@ def test_eval_types_4():
[
Param[Literal["a"], int, Literal["positional"]],
Param[Literal["b"], int],
Param[Literal["c"], int, Literal["default"]],
Param[Literal["c"], int, Never, int],
Param[None, int, Literal["*"]],
Param[Literal["d"], int, Literal["keyword"]],
Param[Literal["e"], int, Literal["default", "keyword"]],
Param[Literal["e"], int, Literal["keyword"], int],
Param[None, int, Literal["**"]],
],
int,
Expand All @@ -172,10 +172,10 @@ def test_eval_types_4():
[
Param[Literal["a"], int, Literal["positional"]],
Param[Literal["b"], int],
Param[Literal["c"], int, Literal["default"]],
Param[Literal["c"], int, Never, int],
Param[None, int, Literal["*"]],
Param[Literal["d"], int, Literal["keyword"]],
Param[Literal["e"], int, Literal["default", "keyword"]],
Param[Literal["e"], int, Literal["keyword"], int],
Param[None, int, Literal["**"]],
],
int,
Expand Down Expand Up @@ -1827,10 +1827,10 @@ def test_callable_to_signature_01():
Params[
Param[None, int],
Param[Literal["b"], int],
Param[Literal["c"], int, Literal["default"]],
Param[Literal["c"], int, Never, int],
Param[None, int, Literal["*"]],
Param[Literal["d"], int, Literal["keyword"]],
Param[Literal["e"], int, Literal["default", "keyword"]],
Param[Literal["e"], int, Literal["keyword"], int],
Param[None, int, Literal["**"]],
],
int,
Expand Down
35 changes: 26 additions & 9 deletions typemap/type_eval/_eval_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import collections.abc
import contextlib
import dataclasses
import enum
import functools
import inspect
import itertools
Expand Down Expand Up @@ -609,11 +610,17 @@ def _callable_type_to_signature(callable_type: object) -> inspect.Signature:
else:
kind = inspect.Parameter.POSITIONAL_OR_KEYWORD

# Handle default value
# Handle default value from the 4th Param arg (D)
default_type = param_args[3] if len(param_args) > 3 else typing.Never
default: typing.Any
if "default" in quals:
# We don't have the actual default value, use a sentinel
default = _DUMMY_DEFAULT
if default_type is not typing.Never:
if (
_typing_inspect.is_literal(default_type)
and len(default_type.__args__) == 1 # type: ignore[union-attr]
):
default = default_type.__args__[0] # type: ignore[union-attr]
else:
default = _DUMMY_DEFAULT
else:
default = inspect.Parameter.empty

Expand Down Expand Up @@ -661,10 +668,11 @@ def fn(*args, **kwargs):


def _is_pos_only(param):
name, _, quals = typing.get_args(param)
args = typing.get_args(param)
name, _, quals = args[0], args[1], args[2]
qual_set = _get_quals(quals)
return "positional" in qual_set or (
name is None and not (_get_quals(quals) & {"*", "**"})
name is None and not (qual_set & {"*", "**"})
)


Expand Down Expand Up @@ -771,13 +779,22 @@ def _ann(x):
quals.append("keyword")
if p.kind == inspect.Parameter.POSITIONAL_ONLY:
quals.append("positional")
if p.default is not empty:
quals.append("default")
ann_type = _ann(ann)
has_default = p.default is not empty
if has_default:
v = p.default
if v is None or isinstance(v, (int, str, bytes, bool, enum.Enum)):
default_type = typing.Literal[(v,)]
else:
default_type = ann_type
else:
default_type = typing.Never
params.append(
Param[
typing.Literal[p.name],
_ann(ann),
ann_type,
typing.Literal[*quals] if quals else typing.Never,
default_type,
]
)

Expand Down
14 changes: 7 additions & 7 deletions typemap/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,23 +198,22 @@ class Member[
type definer = D


ParamQuals = Literal["*", "**", "keyword", "positional", "default"]
ParamQuals = Literal["*", "**", "keyword", "positional"]


@has_associated_types
class Param[N: str | None, T, Q: ParamQuals = typing.Never]:
class Param[N: str | None, T, Q: ParamQuals = typing.Never, D = typing.Never]:
type name = N
type type = T
type quals = Q
type default = D


type PosParam[N: str | None, T] = Param[N, T, Literal["positional"]]
type PosDefaultParam[N: str | None, T] = Param[
N, T, Literal["positional", "default"]
]
type DefaultParam[N: str, T] = Param[N, T, Literal["default"]]
type PosDefaultParam[N: str | None, T] = Param[N, T, Literal["positional"], T]
type DefaultParam[N: str, T] = Param[N, T, typing.Never, T]
type NamedParam[N: str, T] = Param[N, T, Literal["keyword"]]
type NamedDefaultParam[N: str, T] = Param[N, T, Literal["keyword", "default"]]
type NamedDefaultParam[N: str, T] = Param[N, T, Literal["keyword"], T]
type ArgsParam[T] = Param[Literal[None], T, Literal["*"]]
type KwargsParam[T] = Param[Literal[None], T, Literal["**"]]

Expand All @@ -236,6 +235,7 @@ def __class_getitem__(cls, params):
type GetName[T: Member | Param] = T.name
type GetType[T: Member | Param] = T.type
type GetQuals[T: Member | Param] = T.quals
type GetDefault[T: Param] = T.default
type GetInit[T: Member] = T.init
type GetDefiner[T: Member] = T.definer

Expand Down
Loading