Skip to content

Commit b9e9845

Browse files
committed
Add basic TypeVar default validation
1 parent c3cc492 commit b9e9845

File tree

5 files changed

+101
-38
lines changed

5 files changed

+101
-38
lines changed

mypy/message_registry.py

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

mypy/semanal.py

Lines changed: 79 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4096,28 +4096,15 @@ def process_typevar_parameters(
40964096
if has_values:
40974097
self.fail("TypeVar cannot have both values and an upper bound", context)
40984098
return None
4099-
try:
4100-
# We want to use our custom error message below, so we suppress
4101-
# the default error message for invalid types here.
4102-
analyzed = self.expr_to_analyzed_type(
4103-
param_value, allow_placeholder=True, report_invalid_types=False
4104-
)
4105-
if analyzed is None:
4106-
# Type variables are special: we need to place them in the symbol table
4107-
# soon, even if upper bound is not ready yet. Otherwise avoiding
4108-
# a "deadlock" in this common pattern would be tricky:
4109-
# T = TypeVar('T', bound=Custom[Any])
4110-
# class Custom(Generic[T]):
4111-
# ...
4112-
analyzed = PlaceholderType(None, [], context.line)
4113-
upper_bound = get_proper_type(analyzed)
4114-
if isinstance(upper_bound, AnyType) and upper_bound.is_from_error:
4115-
self.fail(message_registry.TYPEVAR_BOUND_MUST_BE_TYPE, param_value)
4116-
# Note: we do not return 'None' here -- we want to continue
4117-
# using the AnyType as the upper bound.
4118-
except TypeTranslationError:
4119-
self.fail(message_registry.TYPEVAR_BOUND_MUST_BE_TYPE, param_value)
4099+
tv_arg = self.get_typevarlike_argument(param_name, param_value, context)
4100+
if tv_arg is None:
4101+
return None
4102+
upper_bound = tv_arg
4103+
elif param_name == "default":
4104+
tv_arg = self.get_typevarlike_argument(param_name, param_value, context)
4105+
if tv_arg is None:
41204106
return None
4107+
default = tv_arg
41214108
elif param_name == "values":
41224109
# Probably using obsolete syntax with values=(...). Explain the current syntax.
41234110
self.fail('TypeVar "values" argument not supported', context)
@@ -4145,6 +4132,35 @@ def process_typevar_parameters(
41454132
variance = INVARIANT
41464133
return variance, upper_bound, default
41474134

4135+
def get_typevarlike_argument(
4136+
self, param_name: str, param_value: Expression, context: Context
4137+
) -> ProperType | None:
4138+
try:
4139+
# We want to use our custom error message below, so we suppress
4140+
# the default error message for invalid types here.
4141+
analyzed = self.expr_to_analyzed_type(
4142+
param_value, allow_placeholder=True, report_invalid_types=False
4143+
)
4144+
if analyzed is None:
4145+
# Type variables are special: we need to place them in the symbol table
4146+
# soon, even if upper bound is not ready yet. Otherwise avoiding
4147+
# a "deadlock" in this common pattern would be tricky:
4148+
# T = TypeVar('T', bound=Custom[Any])
4149+
# class Custom(Generic[T]):
4150+
# ...
4151+
analyzed = PlaceholderType(None, [], context.line)
4152+
typ = get_proper_type(analyzed)
4153+
if isinstance(typ, AnyType) and typ.is_from_error:
4154+
self.fail(
4155+
message_registry.TYPEVAR_ARG_MUST_BE_TYPE.format(param_name), param_value
4156+
)
4157+
# Note: we do not return 'None' here -- we want to continue
4158+
# using the AnyType as the upper bound.
4159+
return typ
4160+
except TypeTranslationError:
4161+
self.fail(message_registry.TYPEVAR_ARG_MUST_BE_TYPE.format(param_name), param_value)
4162+
return None
4163+
41484164
def extract_typevarlike_name(self, s: AssignmentStmt, call: CallExpr) -> str | None:
41494165
if not call:
41504166
return None
@@ -4177,13 +4193,27 @@ def process_paramspec_declaration(self, s: AssignmentStmt) -> bool:
41774193
if name is None:
41784194
return False
41794195

4180-
# ParamSpec is different from a regular TypeVar:
4181-
# arguments are not semantically valid. But, allowed in runtime.
4182-
# So, we need to warn users about possible invalid usage.
4183-
if len(call.args) > 1:
4184-
self.fail("Only the first argument to ParamSpec has defined semantics", s)
4196+
n_values = call.arg_kinds[1:].count(ARG_POS)
4197+
if n_values != 0:
4198+
self.fail("Only the first positional argument to ParamSpec has defined semantics", s)
41854199

41864200
default: Type = AnyType(TypeOfAny.from_omitted_generics)
4201+
for param_value, param_name in zip(
4202+
call.args[1 + n_values :], call.arg_names[1 + n_values :]
4203+
):
4204+
if param_name == "default":
4205+
tv_arg = self.get_typevarlike_argument(param_name, param_value, s)
4206+
if tv_arg is None:
4207+
return False
4208+
default = tv_arg
4209+
else:
4210+
# ParamSpec is different from a regular TypeVar:
4211+
# arguments are not semantically valid. But, allowed in runtime.
4212+
# So, we need to warn users about possible invalid usage.
4213+
self.fail(
4214+
"The variance and bound arguments to ParamSpec do not have defined semantics yet",
4215+
s,
4216+
)
41874217

41884218
# PEP 612 reserves the right to define bound, covariant and contravariant arguments to
41894219
# ParamSpec in a later PEP. If and when that happens, we should do something
@@ -4211,10 +4241,31 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool:
42114241
if not call:
42124242
return False
42134243

4214-
if len(call.args) > 1:
4215-
self.fail("Only the first argument to TypeVarTuple has defined semantics", s)
4244+
n_values = call.arg_kinds[1:].count(ARG_POS)
4245+
if n_values != 0:
4246+
self.fail(
4247+
"Only the first positional argument to TypeVarTuple has defined semantics", s
4248+
)
42164249

42174250
default: Type = AnyType(TypeOfAny.from_omitted_generics)
4251+
for param_value, param_name in zip(
4252+
call.args[1 + n_values :], call.arg_names[1 + n_values :]
4253+
):
4254+
if param_name == "default":
4255+
tv_arg = self.get_typevarlike_argument(param_name, param_value, s)
4256+
if tv_arg is None:
4257+
return False
4258+
default = tv_arg
4259+
if not isinstance(default, UnpackType):
4260+
self.fail(
4261+
"The default argument to TypeVarTuple must be an Unpacked tuple", default
4262+
)
4263+
return False
4264+
else:
4265+
self.fail(
4266+
"The variance and bound arguments to TypeVarTuple do not have defined semantics yet",
4267+
s,
4268+
)
42184269

42194270
if not self.incomplete_feature_enabled(TYPE_VAR_TUPLE, s):
42204271
return False

mypy/types.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3043,6 +3043,8 @@ def visit_type_var(self, t: TypeVarType) -> str:
30433043
s = f"{t.name}`{t.id}"
30443044
if self.id_mapper and t.upper_bound:
30453045
s += f"(upper_bound={t.upper_bound.accept(self)})"
3046+
if t.has_default():
3047+
s += f" = {t.default.accept(self)}"
30463048
return s
30473049

30483050
def visit_param_spec(self, t: ParamSpecType) -> str:
@@ -3058,6 +3060,8 @@ def visit_param_spec(self, t: ParamSpecType) -> str:
30583060
s += f"{t.name_with_suffix()}`{t.id}"
30593061
if t.prefix.arg_types:
30603062
s += "]"
3063+
if t.has_default():
3064+
s += f" = {t.default.accept(self)}"
30613065
return s
30623066

30633067
def visit_parameters(self, t: Parameters) -> str:
@@ -3096,6 +3100,8 @@ def visit_type_var_tuple(self, t: TypeVarTupleType) -> str:
30963100
else:
30973101
# Named type variable type.
30983102
s = f"{t.name}`{t.id}"
3103+
if t.has_default():
3104+
s += f" = {t.default.accept(self)}"
30993105
return s
31003106

31013107
def visit_callable_type(self, t: CallableType) -> str:
@@ -3132,6 +3138,8 @@ def visit_callable_type(self, t: CallableType) -> str:
31323138
if s:
31333139
s += ", "
31343140
s += f"*{n}.args, **{n}.kwargs"
3141+
if param_spec.has_default():
3142+
s += f" = {param_spec.default.accept(self)}"
31353143

31363144
s = f"({s})"
31373145

@@ -3150,12 +3158,16 @@ def visit_callable_type(self, t: CallableType) -> str:
31503158
vals = f"({', '.join(val.accept(self) for val in var.values)})"
31513159
vs.append(f"{var.name} in {vals}")
31523160
elif not is_named_instance(var.upper_bound, "builtins.object"):
3153-
vs.append(f"{var.name} <: {var.upper_bound.accept(self)}")
3161+
vs.append(
3162+
f"{var.name} <: {var.upper_bound.accept(self)}{f' = {var.default.accept(self)}' if var.has_default() else ''}"
3163+
)
31543164
else:
31553165
vs.append(var.name)
31563166
else:
3157-
# For other TypeVarLikeTypes, just use the name
3158-
vs.append(var.name)
3167+
# For other TypeVarLikeTypes, use the name and default
3168+
vs.append(
3169+
f"{var.name}{f' = {var.default.accept(self)}' if var.has_default() else ''}"
3170+
)
31593171
s = f"[{', '.join(vs)}] {s}"
31603172

31613173
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]

test-data/unit/semanal-errors.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1465,7 +1465,7 @@ TVariadic2 = TypeVarTuple('TVariadic2')
14651465
TP = TypeVarTuple('?') # E: String argument 1 "?" to TypeVarTuple(...) does not match variable name "TP"
14661466
TP2: int = TypeVarTuple('TP2') # E: Cannot declare the type of a TypeVar or similar construct
14671467
TP3 = TypeVarTuple() # E: Too few arguments for TypeVarTuple()
1468-
TP4 = TypeVarTuple('TP4', 'TP4') # E: Only the first argument to TypeVarTuple has defined semantics
1468+
TP4 = TypeVarTuple('TP4', 'TP4') # E: Only the first positional argument to TypeVarTuple has defined semantics
14691469
TP5 = TypeVarTuple(t='TP5') # E: TypeVarTuple() expects a string literal as first argument
14701470

14711471
x: TVariadic # E: TypeVarTuple "TVariadic" is unbound

0 commit comments

Comments
 (0)