Skip to content
Prev Previous commit
Next Next commit
Add some safety net; add more tests
  • Loading branch information
ilevkivskyi committed Aug 21, 2022
commit 8eede68f0beb26e1e9f1254b13b7959d921ce3e1
2 changes: 1 addition & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,7 +729,7 @@ def check_overlapping_overloads(self, defn: OverloadedFuncDef) -> None:
# This is to match the direction the implementation's return
# needs to be compatible in.
if impl_type.variables:
impl = unify_generic_callable(
impl: CallableType | None = unify_generic_callable(
# Normalize both before unifying
impl_type.with_unpacked_kwargs(),
sig1.with_unpacked_kwargs(),
Expand Down
2 changes: 1 addition & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,11 +263,11 @@
TypeVarLikeType,
TypeVarType,
UnboundType,
UnpackType,
get_proper_type,
get_proper_types,
invalid_recursive_alias,
is_named_instance,
UnpackType,
)
from mypy.typevars import fill_typevars
from mypy.util import (
Expand Down
21 changes: 12 additions & 9 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
Instance,
LiteralType,
NoneType,
NormalizedCallableType,
Overloaded,
Parameters,
ParamSpecType,
Expand Down Expand Up @@ -626,8 +627,10 @@ def visit_unpack_type(self, left: UnpackType) -> bool:
return False

def visit_parameters(self, left: Parameters) -> bool:
right = self.right
if isinstance(right, Parameters) or isinstance(right, CallableType):
if isinstance(self.right, Parameters) or isinstance(self.right, CallableType):
right = self.right
if isinstance(right, CallableType):
right = right.with_unpacked_kwargs()
return are_parameters_compatible(
left,
right,
Expand Down Expand Up @@ -671,7 +674,7 @@ def visit_callable_type(self, left: CallableType) -> bool:
elif isinstance(right, Parameters):
# this doesn't check return types.... but is needed for is_equivalent
return are_parameters_compatible(
left,
left.with_unpacked_kwargs(),
right,
is_compat=self._is_subtype,
ignore_pos_arg_names=self.subtype_context.ignore_pos_arg_names,
Expand Down Expand Up @@ -1317,8 +1320,8 @@ def g(x: int) -> int: ...


def are_parameters_compatible(
left: Parameters | CallableType,
right: Parameters | CallableType,
left: Parameters | NormalizedCallableType,
right: Parameters | NormalizedCallableType,
*,
is_compat: Callable[[Type, Type], bool],
ignore_pos_arg_names: bool = False,
Expand Down Expand Up @@ -1539,11 +1542,11 @@ def new_is_compat(left: Type, right: Type) -> bool:


def unify_generic_callable(
type: CallableType,
target: CallableType,
type: NormalizedCallableType,
target: NormalizedCallableType,
ignore_return: bool,
return_constraint_direction: int | None = None,
) -> CallableType | None:
) -> NormalizedCallableType | None:
"""Try to unify a generic callable type with another callable type.

Return unified CallableType if successful; otherwise, return None.
Expand Down Expand Up @@ -1580,7 +1583,7 @@ def report(*args: Any) -> None:
)
if had_errors:
return None
return applied
return cast(NormalizedCallableType, applied)


def try_restrict_literal_union(t: UnionType, s: Type) -> list[Type] | None:
Expand Down
32 changes: 22 additions & 10 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Dict,
Iterable,
NamedTuple,
NewType,
Sequence,
TypeVar,
Union,
Expand Down Expand Up @@ -1561,6 +1562,9 @@ def __eq__(self, other: object) -> bool:
return NotImplemented


CT = TypeVar("CT", bound="CallableType")


class CallableType(FunctionLike):
"""Type of a non-overloaded callable object (such as function)."""

Expand Down Expand Up @@ -1658,7 +1662,7 @@ def __init__(
self.unpack_kwargs = unpack_kwargs

def copy_modified(
self,
self: CT,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good candidate for Self type in a future refactoring.

arg_types: Bogus[Sequence[Type]] = _dummy,
arg_kinds: Bogus[list[ArgKind]] = _dummy,
arg_names: Bogus[list[str | None]] = _dummy,
Expand All @@ -1678,8 +1682,8 @@ def copy_modified(
type_guard: Bogus[Type | None] = _dummy,
from_concatenate: Bogus[bool] = _dummy,
unpack_kwargs: Bogus[bool] = _dummy,
) -> CallableType:
return CallableType(
) -> CT:
return type(self)(
arg_types=arg_types if arg_types is not _dummy else self.arg_types,
arg_kinds=arg_kinds if arg_kinds is not _dummy else self.arg_kinds,
arg_names=arg_names if arg_names is not _dummy else self.arg_names,
Expand Down Expand Up @@ -1894,9 +1898,9 @@ def expand_param_spec(
variables=[*variables, *self.variables],
)

def with_unpacked_kwargs(self) -> CallableType:
def with_unpacked_kwargs(self) -> NormalizedCallableType:
if not self.unpack_kwargs:
return self.copy_modified()
return NormalizedCallableType(self.copy_modified())
last_type = get_proper_type(self.arg_types[-1])
assert isinstance(last_type, ProperType) and isinstance(last_type, TypedDictType)
extra_kinds = [
Expand All @@ -1906,11 +1910,13 @@ def with_unpacked_kwargs(self) -> CallableType:
new_arg_kinds = self.arg_kinds[:-1] + extra_kinds
new_arg_names = self.arg_names[:-1] + list(last_type.items)
new_arg_types = self.arg_types[:-1] + list(last_type.items.values())
return self.copy_modified(
arg_kinds=new_arg_kinds,
arg_names=new_arg_names,
arg_types=new_arg_types,
unpack_kwargs=False,
return NormalizedCallableType(
self.copy_modified(
arg_kinds=new_arg_kinds,
arg_names=new_arg_names,
arg_types=new_arg_types,
unpack_kwargs=False,
)
)

def __hash__(self) -> int:
Expand Down Expand Up @@ -1991,6 +1997,12 @@ def deserialize(cls, data: JsonDict) -> CallableType:
)


# This is a little safety net to prevent reckless special-casing of callables
# that can potentially break Unpack[...] with **kwargs.
# TODO: use this in more places in checkexpr.py etc?
NormalizedCallableType = NewType("NormalizedCallableType", CallableType)


class Overloaded(FunctionLike):
"""Overloaded function type T1, ... Tn, where each Ti is CallableType.

Expand Down
15 changes: 15 additions & 0 deletions mypyc/test-data/run-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -1235,3 +1235,18 @@ def g() -> None:
a.pop()

g()

[case testIncompleteFeatureUnpackKwargsCompiled]
from typing_extensions import Unpack, TypedDict

class Person(TypedDict):
name: str
age: int

def foo(**kwargs: Unpack[Person]) -> None:
print(kwargs["name"])

# This is not really supported yet, just test that we behave reasonably.
foo(name='Jennifer', age=38)
[out]
Jennifer
2 changes: 2 additions & 0 deletions mypyc/test/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ def run_case_step(self, testcase: DataDrivenTestCase, incremental_step: int) ->
options.export_types = True
options.preserve_asts = True
options.incremental = self.separate
if "IncompleteFeature" in testcase.name:
options.enable_incomplete_features = True

# Avoid checking modules/packages named 'unchecked', to provide a way
# to test interacting with code we don't have types for.
Expand Down
23 changes: 23 additions & 0 deletions test-data/unit/check-incremental.test
Original file line number Diff line number Diff line change
Expand Up @@ -5989,3 +5989,26 @@ s: str = td["value"]
[out]
[out2]
tmp/b.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "str")

[case testUnpackKwargsSerialize]
import m
[file lib.py]
from typing_extensions import Unpack, TypedDict

class Person(TypedDict):
name: str
age: int

def foo(**kwargs: Unpack[Person]):
...

[file m.py]
from lib import foo
foo(name='Jennifer', age=38)
[file m.py.2]
from lib import foo
foo(name='Jennifer', age="38")
[builtins fixtures/dict.pyi]
[out]
[out2]
tmp/m.py:2: error: Argument "age" to "foo" has incompatible type "str"; expected "int"
2 changes: 1 addition & 1 deletion test-data/unit/check-varargs.test
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,7 @@ class Person(TypedDict, Generic[T]):
def test(cb: CBPerson[T]) -> T: ...

def foo(*, name: str, value: int) -> None: ...
reveal_type(test(foo)) # E: Revealed type is "builtins.int"
reveal_type(test(foo)) # N: Revealed type is "builtins.int"
[builtins fixtures/dict.pyi]

[case testUnpackKwargsOverload]
Expand Down
32 changes: 32 additions & 0 deletions test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
Expand Up @@ -9818,3 +9818,35 @@ x: str
[builtins fixtures/dataclasses.pyi]
[out]
==

[case testUnpackKwargsUpdateFine]
# flags: --enable-incomplete-features
import m
[file shared.py]
from typing_extensions import TypedDict

class Person(TypedDict):
name: str
age: int

[file shared.py.2]
from typing_extensions import TypedDict

class Person(TypedDict):
name: str
age: str

[file lib.py]
from typing_extensions import Unpack
from shared import Person

def foo(**kwargs: Unpack[Person]):
...
[file m.py]
from lib import foo
foo(name='Jennifer', age=38)

[builtins fixtures/dict.pyi]
[out]
==
m.py:2: error: Argument "age" to "foo" has incompatible type "int"; expected "str"