Skip to content

Use TypeVar defaults instead of Any when fixing TypeAlias types (PEP 696) #16825

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

Merged
merged 2 commits into from
Jan 28, 2024
Merged
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
1 change: 1 addition & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4711,6 +4711,7 @@ class LongName(Generic[T]): ...
item = get_proper_type(
set_any_tvars(
alias,
[],
ctx.line,
ctx.column,
self.chk.options,
Expand Down
104 changes: 70 additions & 34 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1927,34 +1927,35 @@ def instantiate_type_alias(
if any(unknown_unpack(a) for a in args):
# This type is not ready to be validated, because of unknown total count.
# Note that we keep the kind of Any for consistency.
return set_any_tvars(node, ctx.line, ctx.column, options, special_form=True)
return set_any_tvars(node, [], ctx.line, ctx.column, options, special_form=True)

exp_len = len(node.alias_tvars)
max_tv_count = len(node.alias_tvars)
act_len = len(args)
if (
exp_len > 0
max_tv_count > 0
and act_len == 0
and not (empty_tuple_index and node.tvar_tuple_index is not None)
):
# Interpret bare Alias same as normal generic, i.e., Alias[Any, Any, ...]
return set_any_tvars(
node,
args,
ctx.line,
ctx.column,
options,
disallow_any=disallow_any,
fail=fail,
unexpanded_type=unexpanded_type,
)
if exp_len == 0 and act_len == 0:
if max_tv_count == 0 and act_len == 0:
if no_args:
assert isinstance(node.target, Instance) # type: ignore[misc]
# Note: this is the only case where we use an eager expansion. See more info about
# no_args aliases like L = List in the docstring for TypeAlias class.
return Instance(node.target.type, [], line=ctx.line, column=ctx.column)
return TypeAliasType(node, [], line=ctx.line, column=ctx.column)
if (
exp_len == 0
max_tv_count == 0
and act_len > 0
and isinstance(node.target, Instance) # type: ignore[misc]
and no_args
Expand All @@ -1967,32 +1968,48 @@ def instantiate_type_alias(
if any(isinstance(a, UnpackType) for a in args):
# A variadic unpack in fixed size alias (fixed unpacks must be flattened by the caller)
fail(message_registry.INVALID_UNPACK_POSITION, ctx, code=codes.VALID_TYPE)
return set_any_tvars(node, ctx.line, ctx.column, options, from_error=True)
correct = act_len == exp_len
return set_any_tvars(node, [], ctx.line, ctx.column, options, from_error=True)
min_tv_count = sum(not tv.has_default() for tv in node.alias_tvars)
fill_typevars = act_len != max_tv_count
correct = min_tv_count <= act_len <= max_tv_count
else:
correct = act_len >= exp_len - 1
min_tv_count = sum(
not tv.has_default() and not isinstance(tv, TypeVarTupleType)
for tv in node.alias_tvars
)
correct = act_len >= min_tv_count
for a in args:
if isinstance(a, UnpackType):
unpacked = get_proper_type(a.type)
if isinstance(unpacked, Instance) and unpacked.type.fullname == "builtins.tuple":
# Variadic tuple is always correct.
correct = True
if not correct:
if use_standard_error:
# This is used if type alias is an internal representation of another type,
# for example a generic TypedDict or NamedTuple.
msg = wrong_type_arg_count(exp_len, exp_len, str(act_len), node.name)
else:
if node.tvar_tuple_index is not None:
exp_len_str = f"at least {exp_len - 1}"
fill_typevars = not correct
if fill_typevars:
if not correct:
if use_standard_error:
# This is used if type alias is an internal representation of another type,
# for example a generic TypedDict or NamedTuple.
msg = wrong_type_arg_count(max_tv_count, max_tv_count, str(act_len), node.name)
else:
exp_len_str = str(exp_len)
msg = (
"Bad number of arguments for type alias,"
f" expected: {exp_len_str}, given: {act_len}"
)
fail(msg, ctx, code=codes.TYPE_ARG)
return set_any_tvars(node, ctx.line, ctx.column, options, from_error=True)
if node.tvar_tuple_index is not None:
msg = (
"Bad number of arguments for type alias,"
f" expected: at least {min_tv_count}, given: {act_len}"
)
elif min_tv_count != max_tv_count:
msg = (
"Bad number of arguments for type alias,"
f" expected between {min_tv_count} and {max_tv_count}, given: {act_len}"
)
else:
msg = (
"Bad number of arguments for type alias,"
f" expected: {min_tv_count}, given: {act_len}"
)
fail(msg, ctx, code=codes.TYPE_ARG)
args = []
return set_any_tvars(node, args, ctx.line, ctx.column, options, from_error=True)
elif node.tvar_tuple_index is not None:
# We also need to check if we are not performing a type variable tuple split.
unpack = find_unpack_in_list(args)
Expand All @@ -2006,7 +2023,7 @@ def instantiate_type_alias(
act_suffix = len(args) - unpack - 1
if act_prefix < exp_prefix or act_suffix < exp_suffix:
fail("TypeVarTuple cannot be split", ctx, code=codes.TYPE_ARG)
return set_any_tvars(node, ctx.line, ctx.column, options, from_error=True)
return set_any_tvars(node, [], ctx.line, ctx.column, options, from_error=True)
# TODO: we need to check args validity w.r.t alias.alias_tvars.
# Otherwise invalid instantiations will be allowed in runtime context.
# Note: in type context, these will be still caught by semanal_typeargs.
Expand All @@ -2025,6 +2042,7 @@ def instantiate_type_alias(

def set_any_tvars(
node: TypeAlias,
args: list[Type],
newline: int,
newcolumn: int,
options: Options,
Expand All @@ -2041,7 +2059,33 @@ def set_any_tvars(
type_of_any = TypeOfAny.special_form
else:
type_of_any = TypeOfAny.from_omitted_generics
if disallow_any and node.alias_tvars:
any_type = AnyType(type_of_any, line=newline, column=newcolumn)

env: dict[TypeVarId, Type] = {}
used_any_type = False
has_type_var_tuple_type = False
for tv, arg in itertools.zip_longest(node.alias_tvars, args, fillvalue=None):
if tv is None:
continue
if arg is None:
if tv.has_default():
arg = tv.default
else:
arg = any_type
used_any_type = True
if isinstance(tv, TypeVarTupleType):
# TODO Handle TypeVarTuple defaults
has_type_var_tuple_type = True
arg = UnpackType(Instance(tv.tuple_fallback.type, [any_type]))
args.append(arg)
env[tv.id] = arg
t = TypeAliasType(node, args, newline, newcolumn)
if not has_type_var_tuple_type:
fixed = expand_type(t, env)
assert isinstance(fixed, TypeAliasType)
t.args = fixed.args

if used_any_type and disallow_any and node.alias_tvars:
assert fail is not None
if unexpanded_type:
type_str = (
Expand All @@ -2057,15 +2101,7 @@ def set_any_tvars(
Context(newline, newcolumn),
code=codes.TYPE_ARG,
)
any_type = AnyType(type_of_any, line=newline, column=newcolumn)

args: list[Type] = []
for tv in node.alias_tvars:
if isinstance(tv, TypeVarTupleType):
args.append(UnpackType(Instance(tv.tuple_fallback.type, [any_type])))
else:
args.append(any_type)
return TypeAliasType(node, args, newline, newcolumn)
return t


def flatten_tvars(lists: list[list[T]]) -> list[T]:
Expand Down
161 changes: 161 additions & 0 deletions test-data/unit/check-typevar-defaults.test
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,164 @@ def func_c4(
# reveal_type(b) # Revealed type is "__main__.ClassC4[builtins.int, builtins.str]" # TODO
reveal_type(c) # N: Revealed type is "__main__.ClassC4[builtins.int, builtins.float]"
[builtins fixtures/tuple.pyi]

[case testTypeVarDefaultsTypeAlias1]
# flags: --disallow-any-generics
from typing import Any, Dict, List, Tuple, TypeVar, Union

T1 = TypeVar("T1")
T2 = TypeVar("T2", default=int)
T3 = TypeVar("T3", default=str)
T4 = TypeVar("T4")

TA1 = Dict[T2, T3]

def func_a1(
a: TA1,
b: TA1[float],
c: TA1[float, float],
d: TA1[float, float, float], # E: Bad number of arguments for type alias, expected between 0 and 2, given: 3
) -> None:
reveal_type(a) # N: Revealed type is "builtins.dict[builtins.int, builtins.str]"
reveal_type(b) # N: Revealed type is "builtins.dict[builtins.float, builtins.str]"
reveal_type(c) # N: Revealed type is "builtins.dict[builtins.float, builtins.float]"
reveal_type(d) # N: Revealed type is "builtins.dict[builtins.int, builtins.str]"

TA2 = Tuple[T1, T2, T3]

def func_a2(
a: TA2, # E: Missing type parameters for generic type "TA2"
b: TA2[float],
c: TA2[float, float],
d: TA2[float, float, float],
e: TA2[float, float, float, float], # E: Bad number of arguments for type alias, expected between 1 and 3, given: 4
) -> None:
reveal_type(a) # N: Revealed type is "Tuple[Any, builtins.int, builtins.str]"
reveal_type(b) # N: Revealed type is "Tuple[builtins.float, builtins.int, builtins.str]"
reveal_type(c) # N: Revealed type is "Tuple[builtins.float, builtins.float, builtins.str]"
reveal_type(d) # N: Revealed type is "Tuple[builtins.float, builtins.float, builtins.float]"
reveal_type(e) # N: Revealed type is "Tuple[Any, builtins.int, builtins.str]"

TA3 = Union[Dict[T1, T2], List[T3]]

def func_a3(
a: TA3, # E: Missing type parameters for generic type "TA3"
b: TA3[float],
c: TA3[float, float],
d: TA3[float, float, float],
e: TA3[float, float, float, float], # E: Bad number of arguments for type alias, expected between 1 and 3, given: 4
) -> None:
reveal_type(a) # N: Revealed type is "Union[builtins.dict[Any, builtins.int], builtins.list[builtins.str]]"
reveal_type(b) # N: Revealed type is "Union[builtins.dict[builtins.float, builtins.int], builtins.list[builtins.str]]"
reveal_type(c) # N: Revealed type is "Union[builtins.dict[builtins.float, builtins.float], builtins.list[builtins.str]]"
reveal_type(d) # N: Revealed type is "Union[builtins.dict[builtins.float, builtins.float], builtins.list[builtins.float]]"
reveal_type(e) # N: Revealed type is "Union[builtins.dict[Any, builtins.int], builtins.list[builtins.str]]"

TA4 = Tuple[T1, T4, T2]

def func_a4(
a: TA4, # E: Missing type parameters for generic type "TA4"
b: TA4[float], # E: Bad number of arguments for type alias, expected between 2 and 3, given: 1
c: TA4[float, float],
d: TA4[float, float, float],
e: TA4[float, float, float, float], # E: Bad number of arguments for type alias, expected between 2 and 3, given: 4
Copy link
Member

Choose a reason for hiding this comment

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

nit: I don't think there should be a colon after "given". However, this looks to be consistent with a bunch of other errors, so we can leave it for now.

A similar runtime error for comparison:

python -c '''
def f(a: int, b: str = ""): ...
f(1, "x", 3)
'''
Traceback (most recent call last):
  File "<string>", line 3, in <module>
TypeError: f() takes from 1 to 2 positional arguments but 3 were given

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree, however for this I tried to modify the existing message as little as possible, thus keeping the colon. If you like, I would suggest that I open a separate PR later to update all cases for "Bad number of arguments for type alias". That would be better since it requires changes to other test files as well.

Copy link
Member

Choose a reason for hiding this comment

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

Sure, sounds good!

) -> None:
reveal_type(a) # N: Revealed type is "Tuple[Any, Any, builtins.int]"
reveal_type(b) # N: Revealed type is "Tuple[Any, Any, builtins.int]"
reveal_type(c) # N: Revealed type is "Tuple[builtins.float, builtins.float, builtins.int]"
reveal_type(d) # N: Revealed type is "Tuple[builtins.float, builtins.float, builtins.float]"
reveal_type(e) # N: Revealed type is "Tuple[Any, Any, builtins.int]"
[builtins fixtures/dict.pyi]

[case testTypeVarDefaultsTypeAlias2]
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
[case testTypeVarDefaultsTypeAlias2]
[case testParamSpecDefaultsTypeAlias]

More informative test case name

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

To be honest, I try to keep the same prefix for all cases to be able to do pytest -k testTypeVarDefaults. Maybe I'm a bit lazy in that regard 😅 Running the whole test file would also work but it's more to type.

pytest mypy/test/testcheck.py::TypeCheckSuite::check-typevar-defaults.test

--
IMO it also makes reading the test cases a bit easier since you can quickly see that all deal with ...TypeAlias. Similarly to the last PR were all used ...Class.

--
In the end it's probably just preference, so if you like me to I'll change it.

# flags: --disallow-any-generics
from typing import Any, Generic, ParamSpec

P1 = ParamSpec("P1")
P2 = ParamSpec("P2", default=[int, str])
P3 = ParamSpec("P3", default=...)

class ClassB1(Generic[P2, P3]): ...
TB1 = ClassB1[P2, P3]

def func_b1(
a: TB1,
b: TB1[[float]],
c: TB1[[float], [float]],
d: TB1[[float], [float], [float]], # E: Bad number of arguments for type alias, expected between 0 and 2, given: 3
) -> None:
reveal_type(a) # N: Revealed type is "__main__.ClassB1[[builtins.int, builtins.str], [*Any, **Any]]"
reveal_type(b) # N: Revealed type is "__main__.ClassB1[[builtins.float], [*Any, **Any]]"
reveal_type(c) # N: Revealed type is "__main__.ClassB1[[builtins.float], [builtins.float]]"
reveal_type(d) # N: Revealed type is "__main__.ClassB1[[builtins.int, builtins.str], [*Any, **Any]]"

class ClassB2(Generic[P1, P2]): ...
TB2 = ClassB2[P1, P2]

def func_b2(
a: TB2, # E: Missing type parameters for generic type "TB2"
b: TB2[[float]],
c: TB2[[float], [float]],
d: TB2[[float], [float], [float]], # E: Bad number of arguments for type alias, expected between 1 and 2, given: 3
) -> None:
reveal_type(a) # N: Revealed type is "__main__.ClassB2[Any, [builtins.int, builtins.str]]"
reveal_type(b) # N: Revealed type is "__main__.ClassB2[[builtins.float], [builtins.int, builtins.str]]"
reveal_type(c) # N: Revealed type is "__main__.ClassB2[[builtins.float], [builtins.float]]"
reveal_type(d) # N: Revealed type is "__main__.ClassB2[Any, [builtins.int, builtins.str]]"
[builtins fixtures/tuple.pyi]

[case testTypeVarDefaultsTypeAlias3]
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
[case testTypeVarDefaultsTypeAlias3]
[case testTypeVarTupleDefaultsTypeAlias]

# flags: --disallow-any-generics
from typing import Tuple, TypeVar
from typing_extensions import TypeVarTuple, Unpack

T1 = TypeVar("T1")
T3 = TypeVar("T3", default=str)

Ts1 = TypeVarTuple("Ts1")
Ts2 = TypeVarTuple("Ts2", default=Unpack[Tuple[int, str]])
Ts3 = TypeVarTuple("Ts3", default=Unpack[Tuple[float, ...]])
Ts4 = TypeVarTuple("Ts4", default=Unpack[Tuple[()]])

TC1 = Tuple[Unpack[Ts2]]

def func_c1(
a: TC1,
b: TC1[float],
) -> None:
# reveal_type(a) # Revealed type is "Tuple[builtins.int, builtins.str]" # TODO
reveal_type(b) # N: Revealed type is "Tuple[builtins.float]"

TC2 = Tuple[T3, Unpack[Ts3]]

def func_c2(
a: TC2,
b: TC2[int],
c: TC2[int, Unpack[Tuple[()]]],
) -> None:
# reveal_type(a) # Revealed type is "Tuple[builtins.str, Unpack[builtins.tuple[builtins.float, ...]]]" # TODO
# reveal_type(b) # Revealed type is "Tuple[builtins.int, Unpack[builtins.tuple[builtins.float, ...]]]" # TODO
reveal_type(c) # N: Revealed type is "Tuple[builtins.int]"

TC3 = Tuple[T3, Unpack[Ts4]]

def func_c3(
a: TC3,
b: TC3[int],
c: TC3[int, Unpack[Tuple[float]]],
) -> None:
# reveal_type(a) # Revealed type is "Tuple[builtins.str]" # TODO
reveal_type(b) # N: Revealed type is "Tuple[builtins.int]"
reveal_type(c) # N: Revealed type is "Tuple[builtins.int, builtins.float]"

TC4 = Tuple[T1, Unpack[Ts1], T3]

def func_c4(
a: TC4, # E: Missing type parameters for generic type "TC4"
b: TC4[int],
c: TC4[int, float],
) -> None:
reveal_type(a) # N: Revealed type is "Tuple[Any, Unpack[builtins.tuple[Any, ...]], builtins.str]"
# reveal_type(b) # Revealed type is "Tuple[builtins.int, builtins.str]" # TODO
reveal_type(c) # N: Revealed type is "Tuple[builtins.int, builtins.float]"
[builtins fixtures/tuple.pyi]