From 4066eff1114d1a710b1420e7bf92663bb7b8fc6c Mon Sep 17 00:00:00 2001 From: franekmagiera Date: Sat, 20 May 2023 19:11:29 +0200 Subject: [PATCH 1/5] Add more tests cases based on PEP 692 --- test-data/unit/check-varargs.test | 136 ++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/test-data/unit/check-varargs.test b/test-data/unit/check-varargs.test index 92b9f7f04f26..0a7f1b763935 100644 --- a/test-data/unit/check-varargs.test +++ b/test-data/unit/check-varargs.test @@ -756,6 +756,19 @@ def foo(arg: bool, **kwargs: Unpack[Person]) -> None: ... reveal_type(foo) # N: Revealed type is "def (arg: builtins.bool, **kwargs: Unpack[TypedDict('__main__.Person', {'name': builtins.str, 'age': builtins.int})])" [builtins fixtures/dict.pyi] +[case testUnpackKwargsTypeInsideOfTheFunction] +from typing import assert_type +from typing_extensions import TypedDict, Unpack + +class Person(TypedDict): + name: str + age: int + +def foo(**kwargs: Unpack[Person]): + assert_type(kwargs, Person) + +[builtins fixtures/dict.pyi] + [case testUnpackOutsideOfKwargs] from typing_extensions import Unpack, TypedDict class Person(TypedDict): @@ -785,6 +798,8 @@ class Person(TypedDict): age: int def foo(name: str, **kwargs: Unpack[Person]) -> None: # E: Overlap between argument names and ** TypedDict items: "name" ... +def bar(name:str, /, **kwargs: Unpack[Person]) -> None: # OK + ... [builtins fixtures/dict.pyi] [case testUnpackWithDuplicateKeywordKwargs] @@ -843,6 +858,42 @@ def bar(**kwargs: Unpack[Square]): bar(side=12) [builtins fixtures/dict.pyi] +[case testUnpackTypedDictRequired] +from typing import Required, TypedDict +from typing_extensions import Unpack + +class Movie(TypedDict, total=False): + name: Required[str] + year: Required[int] + based_on: str + +def foo(**kwargs: Unpack[Movie]): ... + +foo(name="Life of Brian", year=1979) # OK +foo(name="Harry Potter and the Philosopher's Stone", year=2001, based_on="book") # OK + +foo(name="Harry Potter and the Philosopher's Stone", based_on="book") # E: Missing named argument "year" for "foo" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testUnpackTypedDictNotRequired] +from typing import NotRequired, TypedDict +from typing_extensions import Unpack + +class Movie(TypedDict, total=True): + name: str + year: int + based_on: NotRequired[str] + +def foo(**kwargs: Unpack[Movie]): ... + +foo(name="Life of Brian", year=1979) # OK +foo(name="Harry Potter and the Philosopher's Stone", year=2001, based_on="book") # OK + +foo(name="Harry Potter and the Philosopher's Stone", based_on="book") # E: Missing named argument "year" for "foo" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + [case testUnpackUnexpectedKeyword] from typing_extensions import Unpack, TypedDict @@ -1066,3 +1117,88 @@ class C: class D: def __init__(self, **kwds: Unpack[int, str]) -> None: ... # E: Unpack[...] requires exactly one type argument [builtins fixtures/dict.pyi] + +[case testUnpackKwargsAssignmentWhenSourceIsUntyped] +from typing_extensions import Unpack, TypedDict + +class Movie(TypedDict): + name: str + year: int + +def src(**kwargs): ... +def dest(**kwargs: Unpack[Movie]): ... + +dest = src # OK +[builtins fixtures/dict.pyi] + +[case testUnpackKwargsAssignmentWhenSourceHasTraditionallyTypedKwargs] +from typing_extensions import Unpack, TypedDict + +class Vehicle: + ... + +class Car(Vehicle): + ... + +class Motorcycle(Vehicle): + ... + +def src(**kwargs: Vehicle): ... + +class Vehicles(TypedDict): + car: Car + moto: Motorcycle + +def dest(**kwargs: Unpack[Vehicles]): ... + +dest = src # OK +[builtins fixtures/dict.pyi] + +[case testUnpackKwargsAssignmentWhenDestinationHasTraditionallyTypedKwargs] +from typing_extensions import Unpack, TypedDict + +class Vehicle: + ... + +class Car(Vehicle): + ... + +class Motorcycle(Vehicle): + ... + +def dest(**kwargs: Vehicle): ... + +class Vehicles(TypedDict): + car: Car + moto: Motorcycle + +def src(**kwargs: Unpack[Vehicles]): ... + +dest = src # E: Incompatible types in assignment (expression has type "Callable[[KwArg(Vehicles)], Any]", variable has type "Callable[[KwArg(Vehicle)], Any]") +[builtins fixtures/dict.pyi] + +[case testUnpackKwargsAssignmentWhenOnlySourceContainsKwargs] +from typing import NotRequired +from typing_extensions import Unpack, TypedDict + +class Animal(TypedDict): + name: str + +class Dog(Animal): + breed: str + +class Example(TypedDict): + animal: Animal + string: str + number: NotRequired[int] + +def src(**kwargs: Unpack[Example]): ... +def dest(*, animal: Dog, string: str, number: int = ...): ... + +dest = src # OK + +def dest_with_positional_args(animal: Dog, string: str, number: int = ...): ... + +dest_with_positional_args = src # E: Incompatible types in assignment (expression has type "Callable[[KwArg(Example)], Any]", variable has type "Callable[[Dog, str, int], Any]") +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] From 762f50dcb3f4ec581441363a86714b1827a5c4cd Mon Sep 17 00:00:00 2001 From: franekmagiera Date: Thu, 1 Jun 2023 15:38:07 +0200 Subject: [PATCH 2/5] Implement function assignment check when only destination contains kwargs --- mypy/checker.py | 16 ++++++++++++++++ test-data/unit/check-varargs.test | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 09dc0a726b99..6cd94809f036 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3935,8 +3935,24 @@ def check_simple_assignment( f"{lvalue_name} has type", notes=notes, ) + + if isinstance(rvalue_type, CallableType) and isinstance(lvalue_type, CallableType): + self.check_compliance_with_pep692_assignment_rules( + rvalue_type, lvalue_type, context + ) + return rvalue_type + def check_compliance_with_pep692_assignment_rules( + self, source: CallableType, destination: CallableType, context: Context + ) -> None: + # Destination contains kwargs and source doesn't. + if destination.unpack_kwargs and not source.is_kw_arg: + self.fail( + "Incompatible function signatures - variable contains **kwargs and the expression does not. See PEP 692 for more details.", + context, + ) + def check_member_assignment( self, instance_type: Type, attribute_type: Type, rvalue: Expression, context: Context ) -> tuple[Type, Type, bool]: diff --git a/test-data/unit/check-varargs.test b/test-data/unit/check-varargs.test index 0a7f1b763935..deb484a2631a 100644 --- a/test-data/unit/check-varargs.test +++ b/test-data/unit/check-varargs.test @@ -1202,3 +1202,23 @@ def dest_with_positional_args(animal: Dog, string: str, number: int = ...): ... dest_with_positional_args = src # E: Incompatible types in assignment (expression has type "Callable[[KwArg(Example)], Any]", variable has type "Callable[[Dog, str, int], Any]") [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] + +[case testUnpackKwargsAssignmentWhenOnlyDestinationContainsKwargs] +from typing_extensions import Unpack, TypedDict + +class Animal(TypedDict): + name: str + +class Dog(Animal): + breed: str + +def dest(**kwargs: Unpack[Animal]): ... +def src(name: str): ... + +dog: Dog = {"name": "Daisy", "breed": "Labrador"} +animal: Animal = dog + +dest = src # E: Incompatible function signatures - variable contains **kwargs and the expression does not. See PEP 692 for more details. + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] From b622dfb80b02f262a4660e48e63f4634165eb034 Mon Sep 17 00:00:00 2001 From: franekmagiera Date: Thu, 6 Jul 2023 20:49:06 +0200 Subject: [PATCH 3/5] Update docstring for UnpackType --- mypy/types.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/types.py b/mypy/types.py index d050f4cc81a2..83a515d67399 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -994,10 +994,11 @@ def __eq__(self, other: object) -> bool: class UnpackType(ProperType): """Type operator Unpack from PEP646. Can be either with Unpack[] - or unpacking * syntax. + or unpacking * syntax. It can also be used for typing **kwargs (PEP692). The inner type should be either a TypeVarTuple, a constant size - tuple, or a variable length tuple, or a union of one of those. + tuple, or a variable length tuple, or a union of one of those OR a + TypedDict. """ __slots__ = ["type"] From bf2510af06b7fbb6e0f6adbcfc9412d014a4bcef Mon Sep 17 00:00:00 2001 From: franekmagiera Date: Thu, 6 Jul 2023 20:52:11 +0200 Subject: [PATCH 4/5] Add function assignment test when both functions are typed with Unpack --- test-data/unit/check-varargs.test | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test-data/unit/check-varargs.test b/test-data/unit/check-varargs.test index deb484a2631a..c35bab2a50ea 100644 --- a/test-data/unit/check-varargs.test +++ b/test-data/unit/check-varargs.test @@ -1222,3 +1222,25 @@ dest = src # E: Incompatible function signatures - variable contains **kwargs a [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] + +[case testUnpackKwargsAssignmentWhenBothSidesContainKwargsTypedWithUnpack] +from typing_extensions import Unpack, TypedDict + +class Animal(TypedDict): + name: str + +class Dog(Animal): + breed: str + +def accept_animal(**kwargs: Unpack[Animal]): ... +def accept_dog(**kwargs: Unpack[Dog]): ... + +accept_dog = accept_animal # OK + +def also_accept_animal(**kwargs: Unpack[Animal]): ... +def also_accept_dog(**kwargs: Unpack[Dog]): ... + +also_accept_animal = also_accept_dog # E: Incompatible types in assignment (expression has type "Callable[[KwArg(Dog)], Any]", variable has type "Callable[[KwArg(Animal)], Any]") + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] From 2cd2bf822df27b6d7f0a3493684b581fa49b033a Mon Sep 17 00:00:00 2001 From: franekmagiera Date: Thu, 6 Jul 2023 21:00:07 +0200 Subject: [PATCH 5/5] Add test for passing kwargs inside a function to another function --- test-data/unit/check-varargs.test | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test-data/unit/check-varargs.test b/test-data/unit/check-varargs.test index c35bab2a50ea..8686a70b42c2 100644 --- a/test-data/unit/check-varargs.test +++ b/test-data/unit/check-varargs.test @@ -1244,3 +1244,39 @@ also_accept_animal = also_accept_dog # E: Incompatible types in assignment (exp [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] + +[case testPassingKwargsFromFunctionToFunction] +from typing_extensions import Unpack, TypedDict + +class Animal(TypedDict): + name: str + +class Dog(Animal): + breed: str + +def takes_name(name: str): + ... + +dog: Dog = {"name": "Daisy", "breed": "Labrador"} +animal: Animal = dog + +def foo(**kwargs: Unpack[Animal]): + return kwargs["name"] + +def bar(**kwargs: Unpack[Animal]): + takes_name(**kwargs) + +def baz(animal: Animal): + takes_name(**animal) + +def spam(**kwargs: Unpack[Animal]): + baz(kwargs) + +foo(**animal) # OK! foo only expects and uses keywords of 'Animal'. + +bar(**animal) # E: This will fail at runtime because 'breed' keyword will be passed to 'takes_name' as well. + +spam(**animal) # E: Again, 'breed' keyword will be eventually passed to 'takes_name'. + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi]