Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure subtyping works with erased typevartuples in callables #18408

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
22 changes: 18 additions & 4 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1620,6 +1620,18 @@ def are_parameters_compatible(
return True
trivial_suffix = is_trivial_suffix(right) and not is_proper_subtype

# def _(*a: Unpack[tuple[Any, ...]]) allows any number of arguments, not just infinite.
if right_star and isinstance(right_star.typ, UnpackType):
right_star_inner_type = get_proper_type(right_star.typ.type)
trivial_varargs = (
isinstance(right_star_inner_type, Instance)
and right_star_inner_type.type.fullname == "builtins.tuple"
and len(right_star_inner_type.args) == 1
and isinstance(get_proper_type(right_star_inner_type.args[0]), AnyType)
)
else:
trivial_varargs = False

if (
right.arg_kinds == [ARG_STAR]
and isinstance(get_proper_type(right.arg_types[0]), AnyType)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm pretty sure this check is redundant. It would require updating above to just handle *args: Any and handling tuple[Any, ...] here:

mypy/mypy/types.py

Lines 2164 to 2168 in e05770d

if not isinstance(unpacked, TupleType):
# Note that we don't normalize *args: *tuple[X, ...] -> *args: X,
# this should be done once in semanal_typeargs.py for user-defined types,
# and we ourselves rarely construct such type.
return self

I'm just not sure because the comment says the form isn't constructed by mypy but it's the constructed form of any erased typevartuple? I also don't like treating any given *args: Any as finite length, but I think that ship has sailed.

Expand Down Expand Up @@ -1657,14 +1669,16 @@ def are_parameters_compatible(
# Furthermore, if we're checking for compatibility in all cases,
# we confirm that if R accepts an infinite number of arguments,
# L must accept the same.
def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | None) -> bool:
def _incompatible(
left_arg: FormalArgument | None, right_arg: FormalArgument | None, varargs: bool
) -> bool:
if right_arg is None:
return False
if left_arg is None:
return not allow_partial_overlap and not trivial_suffix
return not (allow_partial_overlap or trivial_suffix or (varargs and trivial_varargs))
return not is_compat(right_arg.typ, left_arg.typ)

if _incompatible(left_star, right_star) or _incompatible(left_star2, right_star2):
if _incompatible(left_star, right_star, True) or _incompatible(left_star2, right_star2, False):
return False

# Phase 1b: Check non-star args: for every arg right can accept, left must
Expand All @@ -1690,7 +1704,7 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N
# arguments. Get all further positional args of left, and make sure
# they're more general than the corresponding member in right.
# TODO: are we handling UnpackType correctly here?
if right_star is not None and not trivial_suffix:
if right_star is not None and not trivial_suffix and not trivial_varargs:
# Synthesize an anonymous formal argument for the right
right_by_position = right.try_synthesizing_arg_from_vararg(None)
assert right_by_position is not None
Expand Down
26 changes: 26 additions & 0 deletions test-data/unit/check-generics.test
Original file line number Diff line number Diff line change
Expand Up @@ -3548,3 +3548,29 @@ def foo(x: T):
reveal_type(C) # N: Revealed type is "Overload(def [T, S] (x: builtins.int, y: S`-1) -> __main__.C[__main__.Int[S`-1]], def [T, S] (x: builtins.str, y: S`-1) -> __main__.C[__main__.Str[S`-1]])"
reveal_type(C(0, x)) # N: Revealed type is "__main__.C[__main__.Int[T`-1]]"
reveal_type(C("yes", x)) # N: Revealed type is "__main__.C[__main__.Str[T`-1]]"

[case testTypeVarTupleInCallableInUnion]
from typing import TypeVarTuple, Unpack, Generic, Union
from collections.abc import Callable

Args = TypeVarTuple("Args")

class Built(Generic[Unpack[Args]]):
pass

def example(n: Union[Built[Unpack[Args]], Callable[[Unpack[Args]], None]]) -> Built[Unpack[Args]]: ...

reveal_type(example) # N: Revealed type is "def [Args] (n: Union[__main__.Built[Unpack[Args`-1]], def (*Unpack[Args`-1])]) -> __main__.Built[Unpack[Args`-1]]"

@example
def command1() -> None:
return

reveal_type(command1) # N: Revealed type is "__main__.Built[()]"

@example
def command2(a: int) -> None:
return

reveal_type(command2) # N: Revealed type is "__main__.Built[builtins.int]"
[builtins fixtures/tuple.pyi]
Loading