Skip to content

Commit 9c37a1a

Browse files
committed
Add basic TypeVar defaults validation
1 parent a568f3a commit 9c37a1a

File tree

8 files changed

+257
-46
lines changed

8 files changed

+257
-46
lines changed

mypy/exprtotype.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
Type,
3434
TypeList,
3535
TypeOfAny,
36+
TypeOfTypeList,
3637
UnboundType,
3738
UnionType,
3839
)
@@ -161,9 +162,12 @@ def expr_to_unanalyzed_type(
161162
else:
162163
raise TypeTranslationError()
163164
return CallableArgument(typ, name, arg_const, expr.line, expr.column)
164-
elif isinstance(expr, ListExpr):
165+
elif isinstance(expr, (ListExpr, TupleExpr)):
165166
return TypeList(
166167
[expr_to_unanalyzed_type(t, options, allow_new_syntax, expr) for t in expr.items],
168+
TypeOfTypeList.callable_args
169+
if isinstance(expr, ListExpr)
170+
else TypeOfTypeList.param_spec_defaults,
167171
line=expr.line,
168172
column=expr.column,
169173
)

mypy/message_registry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
181181
INVALID_TYPEVAR_ARG_BOUND: Final = 'Type argument {} of "{}" must be a subtype of {}'
182182
INVALID_TYPEVAR_ARG_VALUE: Final = 'Invalid type argument value for "{}"'
183183
TYPEVAR_VARIANCE_DEF: Final = 'TypeVar "{}" may only be a literal bool'
184-
TYPEVAR_BOUND_MUST_BE_TYPE: Final = 'TypeVar "bound" must be a type'
184+
TYPEVAR_ARG_MUST_BE_TYPE: Final = '{} "{}" must be a type'
185185
TYPEVAR_UNEXPECTED_ARGUMENT: Final = 'Unexpected argument to "TypeVar()"'
186186
UNBOUND_TYPEVAR: Final = (
187187
"A function returning TypeVar should receive at least "

mypy/semanal.py

Lines changed: 123 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4124,28 +4124,17 @@ def process_typevar_parameters(
41244124
if has_values:
41254125
self.fail("TypeVar cannot have both values and an upper bound", context)
41264126
return None
4127-
try:
4128-
# We want to use our custom error message below, so we suppress
4129-
# the default error message for invalid types here.
4130-
analyzed = self.expr_to_analyzed_type(
4131-
param_value, allow_placeholder=True, report_invalid_types=False
4132-
)
4133-
if analyzed is None:
4134-
# Type variables are special: we need to place them in the symbol table
4135-
# soon, even if upper bound is not ready yet. Otherwise avoiding
4136-
# a "deadlock" in this common pattern would be tricky:
4137-
# T = TypeVar('T', bound=Custom[Any])
4138-
# class Custom(Generic[T]):
4139-
# ...
4140-
analyzed = PlaceholderType(None, [], context.line)
4141-
upper_bound = get_proper_type(analyzed)
4142-
if isinstance(upper_bound, AnyType) and upper_bound.is_from_error:
4143-
self.fail(message_registry.TYPEVAR_BOUND_MUST_BE_TYPE, param_value)
4144-
# Note: we do not return 'None' here -- we want to continue
4145-
# using the AnyType as the upper bound.
4146-
except TypeTranslationError:
4147-
self.fail(message_registry.TYPEVAR_BOUND_MUST_BE_TYPE, param_value)
4127+
tv_arg = self.get_typevarlike_argument("TypeVar", param_name, param_value, context)
4128+
if tv_arg is None:
41484129
return None
4130+
upper_bound = tv_arg
4131+
elif param_name == "default":
4132+
tv_arg = self.get_typevarlike_argument(
4133+
"TypeVar", param_name, param_value, context, allow_unbound_tvars=True
4134+
)
4135+
if tv_arg is None:
4136+
return None
4137+
default = tv_arg
41494138
elif param_name == "values":
41504139
# Probably using obsolete syntax with values=(...). Explain the current syntax.
41514140
self.fail('TypeVar "values" argument not supported', context)
@@ -4173,6 +4162,50 @@ def process_typevar_parameters(
41734162
variance = INVARIANT
41744163
return variance, upper_bound, default
41754164

4165+
def get_typevarlike_argument(
4166+
self,
4167+
typevarlike_name: str,
4168+
param_name: str,
4169+
param_value: Expression,
4170+
context: Context,
4171+
*,
4172+
allow_unbound_tvars: bool = False,
4173+
allow_param_spec_literals: bool = False,
4174+
) -> ProperType | None:
4175+
try:
4176+
# We want to use our custom error message below, so we suppress
4177+
# the default error message for invalid types here.
4178+
analyzed = self.expr_to_analyzed_type(
4179+
param_value,
4180+
allow_placeholder=True,
4181+
report_invalid_types=False,
4182+
allow_unbound_tvars=allow_unbound_tvars,
4183+
allow_param_spec_literals=allow_param_spec_literals,
4184+
)
4185+
if analyzed is None:
4186+
# Type variables are special: we need to place them in the symbol table
4187+
# soon, even if upper bound is not ready yet. Otherwise avoiding
4188+
# a "deadlock" in this common pattern would be tricky:
4189+
# T = TypeVar('T', bound=Custom[Any])
4190+
# class Custom(Generic[T]):
4191+
# ...
4192+
analyzed = PlaceholderType(None, [], context.line)
4193+
typ = get_proper_type(analyzed)
4194+
if isinstance(typ, AnyType) and typ.is_from_error:
4195+
self.fail(
4196+
message_registry.TYPEVAR_ARG_MUST_BE_TYPE.format(typevarlike_name, param_name),
4197+
param_value,
4198+
)
4199+
# Note: we do not return 'None' here -- we want to continue
4200+
# using the AnyType as the upper bound.
4201+
return typ
4202+
except TypeTranslationError:
4203+
self.fail(
4204+
message_registry.TYPEVAR_ARG_MUST_BE_TYPE.format(typevarlike_name, param_name),
4205+
param_value,
4206+
)
4207+
return None
4208+
41764209
def extract_typevarlike_name(self, s: AssignmentStmt, call: CallExpr) -> str | None:
41774210
if not call:
41784211
return None
@@ -4205,13 +4238,47 @@ def process_paramspec_declaration(self, s: AssignmentStmt) -> bool:
42054238
if name is None:
42064239
return False
42074240

4208-
# ParamSpec is different from a regular TypeVar:
4209-
# arguments are not semantically valid. But, allowed in runtime.
4210-
# So, we need to warn users about possible invalid usage.
4211-
if len(call.args) > 1:
4212-
self.fail("Only the first argument to ParamSpec has defined semantics", s)
4241+
n_values = call.arg_kinds[1:].count(ARG_POS)
4242+
if n_values != 0:
4243+
self.fail("Only the first positional argument to ParamSpec has defined semantics", s)
42134244

42144245
default: Type = AnyType(TypeOfAny.from_omitted_generics)
4246+
for param_value, param_name in zip(
4247+
call.args[1 + n_values :], call.arg_names[1 + n_values :]
4248+
):
4249+
if param_name == "default":
4250+
tv_arg = self.get_typevarlike_argument(
4251+
"ParamSpec",
4252+
param_name,
4253+
param_value,
4254+
s,
4255+
allow_unbound_tvars=True,
4256+
allow_param_spec_literals=True,
4257+
)
4258+
if tv_arg is None:
4259+
return False
4260+
default = tv_arg
4261+
if isinstance(tv_arg, Parameters):
4262+
for i, arg_type in enumerate(tv_arg.arg_types):
4263+
typ = get_proper_type(arg_type)
4264+
if isinstance(typ, AnyType) and typ.is_from_error:
4265+
self.fail(
4266+
f"Argument {i} of ParamSpec default must be a type", param_value
4267+
)
4268+
elif not isinstance(default, (AnyType, UnboundType)):
4269+
self.fail(
4270+
"The default argument to ParamSpec must be a tuple expression, ellipsis, or a ParamSpec",
4271+
param_value,
4272+
)
4273+
default = AnyType(TypeOfAny.from_error)
4274+
else:
4275+
# ParamSpec is different from a regular TypeVar:
4276+
# arguments are not semantically valid. But, allowed in runtime.
4277+
# So, we need to warn users about possible invalid usage.
4278+
self.fail(
4279+
"The variance and bound arguments to ParamSpec do not have defined semantics yet",
4280+
s,
4281+
)
42154282

42164283
# PEP 612 reserves the right to define bound, covariant and contravariant arguments to
42174284
# ParamSpec in a later PEP. If and when that happens, we should do something
@@ -4245,10 +4312,34 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool:
42454312
if not call:
42464313
return False
42474314

4248-
if len(call.args) > 1:
4249-
self.fail("Only the first argument to TypeVarTuple has defined semantics", s)
4315+
n_values = call.arg_kinds[1:].count(ARG_POS)
4316+
if n_values != 0:
4317+
self.fail(
4318+
"Only the first positional argument to TypeVarTuple has defined semantics", s
4319+
)
42504320

42514321
default: Type = AnyType(TypeOfAny.from_omitted_generics)
4322+
for param_value, param_name in zip(
4323+
call.args[1 + n_values :], call.arg_names[1 + n_values :]
4324+
):
4325+
if param_name == "default":
4326+
tv_arg = self.get_typevarlike_argument(
4327+
"TypeVarTuple", param_name, param_value, s, allow_unbound_tvars=True
4328+
)
4329+
if tv_arg is None:
4330+
return False
4331+
default = tv_arg
4332+
if not isinstance(default, UnpackType):
4333+
self.fail(
4334+
"The default argument to TypeVarTuple must be an Unpacked tuple",
4335+
param_value,
4336+
)
4337+
default = AnyType(TypeOfAny.from_error)
4338+
else:
4339+
self.fail(
4340+
"The variance and bound arguments to TypeVarTuple do not have defined semantics yet",
4341+
s,
4342+
)
42524343

42534344
if not self.incomplete_feature_enabled(TYPE_VAR_TUPLE, s):
42544345
return False
@@ -6348,6 +6439,8 @@ def expr_to_analyzed_type(
63486439
report_invalid_types: bool = True,
63496440
allow_placeholder: bool = False,
63506441
allow_type_any: bool = False,
6442+
allow_unbound_tvars: bool = False,
6443+
allow_param_spec_literals: bool = False,
63516444
) -> Type | None:
63526445
if isinstance(expr, CallExpr):
63536446
# This is a legacy syntax intended mostly for Python 2, we keep it for
@@ -6376,6 +6469,8 @@ def expr_to_analyzed_type(
63766469
report_invalid_types=report_invalid_types,
63776470
allow_placeholder=allow_placeholder,
63786471
allow_type_any=allow_type_any,
6472+
allow_unbound_tvars=allow_unbound_tvars,
6473+
allow_param_spec_literals=allow_param_spec_literals,
63796474
)
63806475

63816476
def analyze_type_expr(self, expr: Expression) -> None:

mypy/typeanal.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
TypedDictType,
7373
TypeList,
7474
TypeOfAny,
75+
TypeOfTypeList,
7576
TypeQuery,
7677
TypeType,
7778
TypeVarLikeType,
@@ -890,10 +891,12 @@ def visit_type_list(self, t: TypeList) -> Type:
890891
else:
891892
return AnyType(TypeOfAny.from_error)
892893
else:
894+
s = "[...]" if t.list_type == TypeOfTypeList.callable_args else "(...)"
893895
self.fail(
894-
'Bracketed expression "[...]" is not valid as a type', t, code=codes.VALID_TYPE
896+
f'Bracketed expression "{s}" is not valid as a type', t, code=codes.VALID_TYPE
895897
)
896-
self.note('Did you mean "List[...]"?', t)
898+
if t.list_type == TypeOfTypeList.callable_args:
899+
self.note('Did you mean "List[...]"?', t)
897900
return AnyType(TypeOfAny.from_error)
898901

899902
def visit_callable_argument(self, t: CallableArgument) -> Type:

mypy/types.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,17 @@ class TypeOfAny:
197197
suggestion_engine: Final = 9
198198

199199

200+
class TypeOfTypeList:
201+
"""This class describes the different types of TypeList."""
202+
203+
__slots__ = ()
204+
205+
# List expressions for callable args
206+
callable_args: Final = 1
207+
# Tuple expressions for ParamSpec defaults
208+
param_spec_defaults: Final = 2
209+
210+
200211
def deserialize_type(data: JsonDict | str) -> Type:
201212
if isinstance(data, str):
202213
return Instance.deserialize(data)
@@ -1011,13 +1022,20 @@ class TypeList(ProperType):
10111022
types before they are processed into Callable types.
10121023
"""
10131024

1014-
__slots__ = ("items",)
1025+
__slots__ = ("items", "list_type")
10151026

10161027
items: list[Type]
10171028

1018-
def __init__(self, items: list[Type], line: int = -1, column: int = -1) -> None:
1029+
def __init__(
1030+
self,
1031+
items: list[Type],
1032+
list_type: int = TypeOfTypeList.callable_args,
1033+
line: int = -1,
1034+
column: int = -1,
1035+
) -> None:
10191036
super().__init__(line, column)
10201037
self.items = items
1038+
self.list_type = list_type
10211039

10221040
def accept(self, visitor: TypeVisitor[T]) -> T:
10231041
assert isinstance(visitor, SyntheticTypeVisitor)
@@ -1031,7 +1049,11 @@ def __hash__(self) -> int:
10311049
return hash(tuple(self.items))
10321050

10331051
def __eq__(self, other: object) -> bool:
1034-
return isinstance(other, TypeList) and self.items == other.items
1052+
return (
1053+
isinstance(other, TypeList)
1054+
and self.items == other.items
1055+
and self.list_type == other.list_type
1056+
)
10351057

10361058

10371059
class UnpackType(ProperType):
@@ -3049,6 +3071,8 @@ def visit_type_var(self, t: TypeVarType) -> str:
30493071
s = f"{t.name}`{t.id}"
30503072
if self.id_mapper and t.upper_bound:
30513073
s += f"(upper_bound={t.upper_bound.accept(self)})"
3074+
if t.has_default():
3075+
s += f" = {t.default.accept(self)}"
30523076
return s
30533077

30543078
def visit_param_spec(self, t: ParamSpecType) -> str:
@@ -3064,6 +3088,8 @@ def visit_param_spec(self, t: ParamSpecType) -> str:
30643088
s += f"{t.name_with_suffix()}`{t.id}"
30653089
if t.prefix.arg_types:
30663090
s += "]"
3091+
if t.has_default():
3092+
s += f" = {t.default.accept(self)}"
30673093
return s
30683094

30693095
def visit_parameters(self, t: Parameters) -> str:
@@ -3102,6 +3128,8 @@ def visit_type_var_tuple(self, t: TypeVarTupleType) -> str:
31023128
else:
31033129
# Named type variable type.
31043130
s = f"{t.name}`{t.id}"
3131+
if t.has_default():
3132+
s += f" = {t.default.accept(self)}"
31053133
return s
31063134

31073135
def visit_callable_type(self, t: CallableType) -> str:
@@ -3138,6 +3166,8 @@ def visit_callable_type(self, t: CallableType) -> str:
31383166
if s:
31393167
s += ", "
31403168
s += f"*{n}.args, **{n}.kwargs"
3169+
if param_spec.has_default():
3170+
s += f" = {param_spec.default.accept(self)}"
31413171

31423172
s = f"({s})"
31433173

@@ -3156,12 +3186,18 @@ def visit_callable_type(self, t: CallableType) -> str:
31563186
vals = f"({', '.join(val.accept(self) for val in var.values)})"
31573187
vs.append(f"{var.name} in {vals}")
31583188
elif not is_named_instance(var.upper_bound, "builtins.object"):
3159-
vs.append(f"{var.name} <: {var.upper_bound.accept(self)}")
3189+
vs.append(
3190+
f"{var.name} <: {var.upper_bound.accept(self)}{f' = {var.default.accept(self)}' if var.has_default() else ''}"
3191+
)
31603192
else:
3161-
vs.append(var.name)
3193+
vs.append(
3194+
f"{var.name}{f' = {var.default.accept(self)}' if var.has_default() else ''}"
3195+
)
31623196
else:
3163-
# For other TypeVarLikeTypes, just use the name
3164-
vs.append(var.name)
3197+
# For other TypeVarLikeTypes, use the name and default
3198+
vs.append(
3199+
f"{var.name}{f' = {var.default.accept(self)}' if var.has_default() else ''}"
3200+
)
31653201
s = f"[{', '.join(vs)}] {s}"
31663202

31673203
return f"def {s}"

test-data/unit/check-parameter-specification.test

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ P = ParamSpec('P')
66
[case testInvalidParamSpecDefinitions]
77
from typing import ParamSpec
88

9-
P1 = ParamSpec("P1", covariant=True) # E: Only the first argument to ParamSpec has defined semantics
10-
P2 = ParamSpec("P2", contravariant=True) # E: Only the first argument to ParamSpec has defined semantics
11-
P3 = ParamSpec("P3", bound=int) # E: Only the first argument to ParamSpec has defined semantics
12-
P4 = ParamSpec("P4", int, str) # E: Only the first argument to ParamSpec has defined semantics
13-
P5 = ParamSpec("P5", covariant=True, bound=int) # E: Only the first argument to ParamSpec has defined semantics
9+
P1 = ParamSpec("P1", covariant=True) # E: The variance and bound arguments to ParamSpec do not have defined semantics yet
10+
P2 = ParamSpec("P2", contravariant=True) # E: The variance and bound arguments to ParamSpec do not have defined semantics yet
11+
P3 = ParamSpec("P3", bound=int) # E: The variance and bound arguments to ParamSpec do not have defined semantics yet
12+
P4 = ParamSpec("P4", int, str) # E: Only the first positional argument to ParamSpec has defined semantics
13+
P5 = ParamSpec("P5", covariant=True, bound=int) # E: The variance and bound arguments to ParamSpec do not have defined semantics yet
1414
[builtins fixtures/paramspec.pyi]
1515

1616
[case testParamSpecLocations]

0 commit comments

Comments
 (0)