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
19 changes: 18 additions & 1 deletion mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -941,7 +941,24 @@ def from_funcitem(stub: nodes.FuncItem) -> Signature[nodes.Argument]:
elif stub_arg.kind == nodes.ARG_STAR:
stub_sig.varpos = stub_arg
elif stub_arg.kind == nodes.ARG_STAR2:
stub_sig.varkw = stub_arg
if stub_arg.variable.type is not None and isinstance(
(typed_dict_arg := mypy.types.get_proper_type(stub_arg.variable.type)),
mypy.types.TypedDictType,
):
for key_name, key_type in typed_dict_arg.items.items():
stub_sig.kwonly[key_name] = nodes.Argument(
nodes.Var(key_name, key_type),
type_annotation=key_type,
initializer=(
nodes.EllipsisExpr()
if key_name not in typed_dict_arg.required_keys
else None
),
kind=nodes.ARG_NAMED,
pos_only=False,
Comment on lines +949 to +958
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing this locally, I think you need to set kind to ARG_NAMED_OPT too for optional parameters to be handled correctly.

Suggested change
stub_sig.kwonly[key_name] = nodes.Argument(
nodes.Var(key_name, key_type),
type_annotation=key_type,
initializer=(
nodes.EllipsisExpr()
if key_name not in typed_dict_arg.required_keys
else None
),
kind=nodes.ARG_NAMED,
pos_only=False,
optional = key_name not in typed_dict_arg.required_keys
stub_sig.kwonly[key_name] = nodes.Argument(
nodes.Var(key_name, key_type),
type_annotation=key_type,
initializer=nodes.EllipsisExpr() if optional else None,
kind=nodes.ARG_NAMED_OPT if optional else nodes.ARG_NAMED,
pos_only=False,

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I'm also not sure if setting initializer=nodes.EllipsisExpr() is necessary. I wasn't able to find a difference between using EllipsisExpr vs always using None)

)
else:
stub_sig.varkw = stub_arg
else:
raise AssertionError
return stub_sig
Expand Down
22 changes: 22 additions & 0 deletions mypy/test/teststubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def __getitem__(self, typeargs: Any) -> object: ...
Final = 0
Literal = 0
TypedDict = 0
Unpack = 0

class TypeVar:
def __init__(self, name, covariant: bool = ..., contravariant: bool = ...) -> None: ...
Expand Down Expand Up @@ -765,6 +766,27 @@ def test_varargs_varkwargs(self) -> Iterator[Case]:
error="k6",
)

@collect_cases
def test_kwargs_unpack_typeddict(self) -> Iterator[Case]:
yield Case(
stub="""
from typing import TypedDict, Unpack

class _Args(TypedDict):
Comment on lines +773 to +775
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that I won't have to update #19574 again :-)

Suggested change
from typing import TypedDict, Unpack
class _Args(TypedDict):
from typing import TypedDict, Unpack, type_check_only
@type_check_only
class _Args(TypedDict):

a: int
b: int

def f1(**kwargs: Unpack[_Args]) -> None: ...
""",
runtime="def f1(*, a, b): pass",
error=None,
)
yield Case(
stub="def f2(**kwargs: Unpack[_Args]) -> None: ...",
runtime="def f2(*, a, c): pass",
error="f2",
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest adding some cases for mismatched optional parameters (e.g. optional in the TypedDict but required at runtime, and vice versa)


@collect_cases
def test_overload(self) -> Iterator[Case]:
yield Case(
Expand Down
Loading