Skip to content

Commit

Permalink
Added test for **kwargs with Unpack TypedDict annotation.
Browse files Browse the repository at this point in the history
  • Loading branch information
erictraut committed Dec 27, 2023
1 parent c797202 commit 3ec7eff
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 8 deletions.
23 changes: 23 additions & 0 deletions conformance/results/mypy/callables_kwargs.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
conformant = "Pass"
output = """
callables_kwargs.py:22: note: "func1" defined here
callables_kwargs.py:43: error: Missing named argument "v1" for "func1" [call-arg]
callables_kwargs.py:43: error: Missing named argument "v3" for "func1" [call-arg]
callables_kwargs.py:48: error: Unexpected keyword argument "v4" for "func1" [call-arg]
callables_kwargs.py:49: error: Too many positional arguments for "func1" [misc]
callables_kwargs.py:55: error: Argument 1 to "func1" has incompatible type "**dict[str, str]"; expected "int" [arg-type]
callables_kwargs.py:58: error: Argument 1 to "func1" has incompatible type "**dict[str, object]"; expected "int" [arg-type]
callables_kwargs.py:58: error: Argument 1 to "func1" has incompatible type "**dict[str, object]"; expected "str" [arg-type]
callables_kwargs.py:60: error: "func1" gets multiple values for keyword argument "v1" [misc]
callables_kwargs.py:61: error: "func2" gets multiple values for keyword argument "v3" [misc]
callables_kwargs.py:61: error: Argument 1 to "func2" has incompatible type "int"; expected "str" [arg-type]
callables_kwargs.py:62: error: "func2" gets multiple values for keyword argument "v1" [misc]
callables_kwargs.py:98: error: Incompatible types in assignment (expression has type "Callable[[KwArg(TD2)], None]", variable has type "TDProtocol3") [assignment]
callables_kwargs.py:98: note: "TDProtocol3.__call__" has type "Callable[[NamedArg(int, 'v1'), NamedArg(int, 'v2'), NamedArg(str, 'v3')], None]"
callables_kwargs.py:99: error: Incompatible types in assignment (expression has type "Callable[[KwArg(TD2)], None]", variable has type "TDProtocol4") [assignment]
callables_kwargs.py:99: note: "TDProtocol4.__call__" has type "Callable[[NamedArg(int, 'v1')], None]"
callables_kwargs.py:100: error: Incompatible types in assignment (expression has type "Callable[[KwArg(TD2)], None]", variable has type "TDProtocol5") [assignment]
callables_kwargs.py:100: note: "TDProtocol5.__call__" has type "Callable[[Arg(int, 'v1'), Arg(str, 'v3')], None]"
callables_kwargs.py:109: error: Overlap between argument names and ** TypedDict items: "v1" [misc]
callables_kwargs.py:121: error: Unpack item in ** argument must be a TypedDict [misc]
"""
2 changes: 1 addition & 1 deletion conformance/results/mypy/version.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version = "mypy 1.8.0"
test_duration = 0.33256983757019043
test_duration = 0.4829740524291992
12 changes: 12 additions & 0 deletions conformance/results/pyre/callables_kwargs.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
conformant = "Unsupported"
note = """
Does not understand Unpack in the context of **kwargs annotation.
"""
output = """
callables_kwargs.py:22:20 Undefined or invalid type [11]: Annotation `Unpack` is not defined as a type.
callables_kwargs.py:49:4 Too many arguments [19]: Call `func1` expects 1 positional argument, 4 were provided.
callables_kwargs.py:59:12 Incompatible parameter type [6]: In call `func2`, for 1st positional argument, expected `str` but got `object`.
callables_kwargs.py:61:10 Incompatible parameter type [6]: In call `func2`, for 1st positional argument, expected `str` but got `int`.
callables_kwargs.py:62:18 Incompatible parameter type [6]: In call `func2`, for 2nd positional argument, expected `str` but got `object`.
callables_kwargs.py:121:20 Invalid type variable [34]: The type variable `Variable[T (bound to callables_kwargs.TD2)]` isn't present in the function's parameters.
"""
2 changes: 1 addition & 1 deletion conformance/results/pyre/version.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version = "pyre 0.9.19"
test_duration = 1.4521031379699707
test_duration = 1.5891530513763428
30 changes: 30 additions & 0 deletions conformance/results/pyright/callables_kwargs.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
conformant = "Pass"
output = """
callables_kwargs.py:26:5 - error: Could not access item in TypedDict
  "v2" is not a required key in "*TD2", so access may result in runtime exception (reportTypedDictNotRequiredAccess)
callables_kwargs.py:43:5 - error: Arguments missing for parameters "v1", "v3" (reportGeneralTypeIssues)
callables_kwargs.py:48:32 - error: No parameter named "v4" (reportGeneralTypeIssues)
callables_kwargs.py:49:11 - error: Expected 0 positional arguments (reportGeneralTypeIssues)
callables_kwargs.py:55:13 - error: Argument of type "str" cannot be assigned to parameter "v1" of type "int" in function "func1"
  "str" is incompatible with "int" (reportGeneralTypeIssues)
callables_kwargs.py:60:19 - error: Unable to match unpacked TypedDict argument to parameters
  Parameter "v1" is already assigned (reportGeneralTypeIssues)
callables_kwargs.py:61:16 - error: Unable to match unpacked TypedDict argument to parameters
  Parameter "v3" is already assigned (reportGeneralTypeIssues)
callables_kwargs.py:62:19 - error: Unable to match unpacked TypedDict argument to parameters
  Parameter "v1" is already assigned (reportGeneralTypeIssues)
callables_kwargs.py:98:19 - error: Expression of type "(**kwargs: **TD2) -> None" cannot be assigned to declared type "TDProtocol3"
  Type "(**kwargs: **TD2) -> None" cannot be assigned to type "(*, v1: int, v2: int, v3: str) -> None"
    Keyword parameter "v2" of type "int" cannot be assigned to type "str"
      "int" is incompatible with "str" (reportGeneralTypeIssues)
callables_kwargs.py:99:19 - error: Expression of type "(**kwargs: **TD2) -> None" cannot be assigned to declared type "TDProtocol4"
  Type "(**kwargs: **TD2) -> None" cannot be assigned to type "(*, v1: int) -> None"
    Keyword parameter "v3" is missing in destination (reportGeneralTypeIssues)
callables_kwargs.py:100:19 - error: Expression of type "(**kwargs: **TD2) -> None" cannot be assigned to declared type "TDProtocol5"
  Type "(**kwargs: **TD2) -> None" cannot be assigned to type "(v1: int, v3: str) -> None"
    Function accepts too many positional parameters; expected 0 but received 2
      Keyword parameter "v1" is missing in destination
      Keyword parameter "v3" is missing in destination (reportGeneralTypeIssues)
callables_kwargs.py:109:30 - error: Typed dictionary overlaps with keyword parameter: v1 (reportGeneralTypeIssues)
callables_kwargs.py:121:21 - error: Expected TypedDict type argument for Unpack (reportGeneralTypeIssues)
"""
2 changes: 1 addition & 1 deletion conformance/results/pyright/version.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version = "pyright 1.1.343"
test_duration = 0.8812670707702637
test_duration = 0.8890669345855713
48 changes: 48 additions & 0 deletions conformance/results/pytype/callables_kwargs.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
conformant = "Unsupported"
note = """
Does not understand Unpack in the context of **kwargs annotation.
"""
output = """
File "callables_kwargs.py", line 10, in <module>: typing.NotRequired not supported yet [not-supported-yet]
File "callables_kwargs.py", line 10, in <module>: typing.Required not supported yet [not-supported-yet]
File "callables_kwargs.py", line 10, in <module>: typing.Unpack not supported yet [not-supported-yet]
File "callables_kwargs.py", line 24, in func1: Unpack[TD2] [assert-type]
Expected: int
Actual: Unpack[TD2]
File "callables_kwargs.py", line 30, in func1: Unpack[TD2] [assert-type]
Expected: str
Actual: Unpack[TD2]
File "callables_kwargs.py", line 33, in func1: Unpack[TD2] [assert-type]
Expected: str
Actual: Unpack[TD2]
File "callables_kwargs.py", line 39, in func2: Dict[str, Unpack[TD1]] [assert-type]
Expected: TD1
Actual: Dict[str, Unpack[TD1]]
File "callables_kwargs.py", line 44, in func3: Function func1 was called with the wrong arguments [wrong-arg-types]
Expected: (v1: Unpack[TD2], ...)
Actually passed: (v1: int, ...)
File "callables_kwargs.py", line 46, in func3: Function TD2.__init__ was called with the wrong arguments [wrong-arg-types]
Expected: (*, v1: Required[int], ...)
Actually passed: (v1: int, ...)
File "callables_kwargs.py", line 48, in func3: Function func1 was called with the wrong arguments [wrong-arg-types]
Expected: (v1: Unpack[TD2], ...)
Actually passed: (v1: int, ...)
File "callables_kwargs.py", line 49, in func3: Function func1 expects 0 arg(s), got 3 [wrong-arg-count]
Expected: (**kwargs)
Actually passed: (_, _, _)
File "callables_kwargs.py", line 55, in func3: Function func1 was called with the wrong arguments [wrong-arg-types]
Expected: (**kwargs: Mapping[str, Unpack[TD2]])
Actually passed: (kwargs: Dict[str, str])
File "callables_kwargs.py", line 58, in func3: Function func1 was called with the wrong arguments [wrong-arg-types]
Expected: (v1: Unpack[TD2], ...)
Actually passed: (v1: int, ...)
File "callables_kwargs.py", line 60, in func3: Function func1 was called with the wrong arguments [wrong-arg-types]
Expected: (v1: Unpack[TD2], ...)
Actually passed: (v1: int, ...)
File "callables_kwargs.py", line 61, in func3: Function func2 was called with the wrong arguments [wrong-arg-types]
Expected: (v3: str, ...)
Actually passed: (v3: int, ...)
File "callables_kwargs.py", line 62, in func3: Function func2 was called with the wrong arguments [wrong-arg-types]
Expected: (v3, v1: Unpack[TD1], ...)
Actually passed: (v1: int, ...)
"""
2 changes: 1 addition & 1 deletion conformance/results/pytype/version.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version = "pytype 2023.12.18"
test_duration = 40.140138149261475
test_duration = 41.63518166542053
12 changes: 8 additions & 4 deletions conformance/results/results.html
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
<header>
<h3>Python Type System Conformance Test Results</h3>
</header>
<div class='tc-header'><span class='tc-name'>mypy 1.8.0<span class='tc-time'>(0.33sec)</span>
<div class='tc-header'><span class='tc-name'>mypy 1.8.0<span class='tc-time'>(0.48sec)</span>
</div>
<div class="table_container"><table>
<tr><th class="column spacer" colspan="4"></th></tr>
Expand All @@ -153,6 +153,7 @@ <h3>Python Type System Conformance Test Results</h3>
<tr><th class="column" colspan="4">
<a class="test_group" href="https://typing.readthedocs.io/en/latest/spec/callables.html">Callables</a></th></tr>
<tr><th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th><th class="column col1">callables_annotation</th><th class="column col2 conformant">Pass</th><th class="column col3"></th></tr>
<tr><th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th><th class="column col1">callables_kwargs</th><th class="column col2 conformant">Pass</th><th class="column col3"></th></tr>
<tr><th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th><th class="column col1">callables_protocol</th><th class="column col2 conformant">Pass</th><th class="column col3"></th></tr>
<tr><th class="column spacer" colspan="4"></th></tr>
<tr><th class="column" colspan="4">
Expand Down Expand Up @@ -187,7 +188,7 @@ <h3>Python Type System Conformance Test Results</h3>
<tr><th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th><th class="column col1">narrowing_typeguard</th><th class="column col2 conformant">Pass</th><th class="column col3"></th></tr>
<tr><th class="column spacer" colspan="4"></th></tr>
</table></div>
<div class='tc-header'><span class='tc-name'>pyright 1.1.343<span class='tc-time'>(0.88sec)</span>
<div class='tc-header'><span class='tc-name'>pyright 1.1.343<span class='tc-time'>(0.89sec)</span>
</div>
<div class="table_container"><table>
<tr><th class="column spacer" colspan="4"></th></tr>
Expand All @@ -213,6 +214,7 @@ <h3>Python Type System Conformance Test Results</h3>
<tr><th class="column" colspan="4">
<a class="test_group" href="https://typing.readthedocs.io/en/latest/spec/callables.html">Callables</a></th></tr>
<tr><th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th><th class="column col1">callables_annotation</th><th class="column col2 conformant">Pass</th><th class="column col3"></th></tr>
<tr><th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th><th class="column col1">callables_kwargs</th><th class="column col2 conformant">Pass</th><th class="column col3"></th></tr>
<tr><th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th><th class="column col1">callables_protocol</th><th class="column col2 partially-conformant">Partial</th><th class="column col3">Does not report type incompatibility for callback protocol with positional-only parameters.<br></th></tr>
<tr><th class="column spacer" colspan="4"></th></tr>
<tr><th class="column" colspan="4">
Expand Down Expand Up @@ -247,7 +249,7 @@ <h3>Python Type System Conformance Test Results</h3>
<tr><th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th><th class="column col1">narrowing_typeguard</th><th class="column col2 conformant">Pass</th><th class="column col3"></th></tr>
<tr><th class="column spacer" colspan="4"></th></tr>
</table></div>
<div class='tc-header'><span class='tc-name'>pyre 0.9.19<span class='tc-time'>(1.45sec)</span>
<div class='tc-header'><span class='tc-name'>pyre 0.9.19<span class='tc-time'>(1.59sec)</span>
</div>
<div class="table_container"><table>
<tr><th class="column spacer" colspan="4"></th></tr>
Expand All @@ -273,6 +275,7 @@ <h3>Python Type System Conformance Test Results</h3>
<tr><th class="column" colspan="4">
<a class="test_group" href="https://typing.readthedocs.io/en/latest/spec/callables.html">Callables</a></th></tr>
<tr><th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th><th class="column col1">callables_annotation</th><th class="column col2 partially-conformant">Partial</th><th class="column col3">Does not evaluate correct type for `*args: int` parameter.<br>Does not reject illegal form `Callable[[...], int]`.<br></th></tr>
<tr><th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th><th class="column col1">callables_kwargs</th><th class="column col2 not-conformant">Unsupported</th><th class="column col3"></th></tr>
<tr><th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th><th class="column col1">callables_protocol</th><th class="column col2 partially-conformant">Partial</th><th class="column col3">Does not correctly handle callback protocol that declares attributes in all functions.<br>Does not report type incompatibility for callback protocol with positional-only parameters.<br>Incorrectly reports type compatibility error with callback that has *args and **kwargs.<br>Does not report type incompatibility for callback missing a default argument for positional parameter.<br>Does not report type incompatibility for callback missing a default argument for keyword parameter.<br></th></tr>
<tr><th class="column spacer" colspan="4"></th></tr>
<tr><th class="column" colspan="4">
Expand Down Expand Up @@ -307,7 +310,7 @@ <h3>Python Type System Conformance Test Results</h3>
<tr><th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th><th class="column col1">narrowing_typeguard</th><th class="column col2 partially-conformant">Partial</th><th class="column col3">Does not support `tuple` in `assert_type` call.<br>Does not reject TypeGuard method with too few parameters.<br></th></tr>
<tr><th class="column spacer" colspan="4"></th></tr>
</table></div>
<div class='tc-header'><span class='tc-name'>pytype 2023.12.18<span class='tc-time'>(40.14sec)</span>
<div class='tc-header'><span class='tc-name'>pytype 2023.12.18<span class='tc-time'>(41.64sec)</span>
</div>
<div class="table_container"><table>
<tr><th class="column spacer" colspan="4"></th></tr>
Expand All @@ -333,6 +336,7 @@ <h3>Python Type System Conformance Test Results</h3>
<tr><th class="column" colspan="4">
<a class="test_group" href="https://typing.readthedocs.io/en/latest/spec/callables.html">Callables</a></th></tr>
<tr><th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th><th class="column col1">callables_annotation</th><th class="column col2 conformant">Pass</th><th class="column col3"></th></tr>
<tr><th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th><th class="column col1">callables_kwargs</th><th class="column col2 not-conformant">Unsupported</th><th class="column col3"></th></tr>
<tr><th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th><th class="column col1">callables_protocol</th><th class="column col2 not-conformant">Unsupported</th><th class="column col3">Does not properly handle type compatibility checks with callback protocols.<br></th></tr>
<tr><th class="column spacer" colspan="4"></th></tr>
<tr><th class="column" colspan="4">
Expand Down
123 changes: 123 additions & 0 deletions conformance/tests/callables_kwargs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""
Tests the use of an unpacked TypedDict for annotating **kwargs.
"""

# Specification: https://typing.readthedocs.io/en/latest/spec/callables.html#unpack-for-keyword-arguments

# This sample tests the handling of Unpack[TypedDict] when used with
# a **kwargs parameter in a function signature.

from typing import Protocol, TypeVar, TypedDict, NotRequired, Required, Unpack, assert_type


class TD1(TypedDict):
v1: Required[int]
v2: NotRequired[str]


class TD2(TD1):
v3: Required[str]


def func1(**kwargs: Unpack[TD2]) -> None:
v1 = kwargs["v1"]
assert_type(v1, int)

kwargs["v2"] # Type error: v2 may not be present

if "v2" in kwargs:
v2 = kwargs["v2"]
assert_type(v2, str)

v3 = kwargs["v3"]
assert_type(v3, str)


def func2(v3: str, **kwargs: Unpack[TD1]) -> None:
# > When Unpack is used, type checkers treat kwargs inside the function
# > body as a TypedDict.
assert_type(kwargs, TD1)


def func3() -> None:
func1() # Type error: missing required keyword args
func1(v1=1, v2="", v3="5") # OK

td2 = TD2(v1=2, v3="4")
func1(**td2) # OK
func1(v1=1, v2="", v3="5", v4=5) # Type error: v4 is not in TD2
func1(1, "", "5") # Type error: args not passed by position

# > Passing a dictionary of type dict[str, object] as a **kwargs argument
# > to a function that has **kwargs annotated with Unpack must generate a
# > type checker error.
my_dict: dict[str, str] = {}
func1(**my_dict) # Type error: untyped dict

d1 = {"v1": 2, "v3": "4", "v4": 4}
func1(**d1) # OK or Type error (spec allows either)
func2(**td2) # OK
func1(v1=2, **td2) # Type error: v1 is already specified
func2(1, **td2) # Type error: v1 is already specified
func2(v1=1, **td2) # Type error: v1 is already specified


class TDProtocol1(Protocol):
def __call__(self, *, v1: int, v3: str) -> None:
...


class TDProtocol2(Protocol):
def __call__(self, *, v1: int, v3: str, v2: str = "") -> None:
...


class TDProtocol3(Protocol):
def __call__(self, *, v1: int, v2: int, v3: str) -> None:
...


class TDProtocol4(Protocol):
def __call__(self, *, v1: int) -> None:
...


class TDProtocol5(Protocol):
def __call__(self, v1: int, v3: str) -> None:
...


class TDProtocol6(Protocol):
def __call__(self, **kwargs: Unpack[TD2]) -> None:
...

# Specification: https://typing.readthedocs.io/en/latest/spec/callables.html#assignment

v1: TDProtocol1 = func1 # OK
v2: TDProtocol2 = func1 # OK
v3: TDProtocol3 = func1 # Type error: v2 is wrong type
v4: TDProtocol4 = func1 # Type error: v3 is missing
v5: TDProtocol5 = func1 # Type error: params are positional
v6: TDProtocol6 = func1 # OK


def func4(v1: int, /, **kwargs: Unpack[TD2]) -> None:
...


# Type error: parameter v1 overlaps with the TypedDict.
def func5(v1: int, **kwargs: Unpack[TD2]) -> None:
...


T = TypeVar("T", bound=TD2)

# > TypedDict is the only permitted heterogeneous type for typing **kwargs.
# > Therefore, in the context of typing **kwargs, using Unpack with types other
# > than TypedDict should not be allowed and type checkers should generate
# > errors in such cases.

# Type error: unpacked value must be a TypedDict, not a TypeVar bound to TypedDict.
def func6(**kwargs: Unpack[T]) -> None:
...

0 comments on commit 3ec7eff

Please sign in to comment.