Skip to content

Commit

Permalink
Add basic TypeVar defaults validation
Browse files Browse the repository at this point in the history
  • Loading branch information
cdce8p committed May 6, 2023
1 parent e5d9c3c commit 5c29737
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 46 deletions.
6 changes: 5 additions & 1 deletion mypy/exprtotype.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
Type,
TypeList,
TypeOfAny,
TypeOfTypeList,
UnboundType,
UnionType,
)
Expand Down Expand Up @@ -161,9 +162,12 @@ def expr_to_unanalyzed_type(
else:
raise TypeTranslationError()
return CallableArgument(typ, name, arg_const, expr.line, expr.column)
elif isinstance(expr, ListExpr):
elif isinstance(expr, (ListExpr, TupleExpr)):
return TypeList(
[expr_to_unanalyzed_type(t, options, allow_new_syntax, expr) for t in expr.items],
TypeOfTypeList.callable_args
if isinstance(expr, ListExpr)
else TypeOfTypeList.param_spec_defaults,
line=expr.line,
column=expr.column,
)
Expand Down
2 changes: 1 addition & 1 deletion mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
INVALID_TYPEVAR_ARG_BOUND: Final = 'Type argument {} of "{}" must be a subtype of {}'
INVALID_TYPEVAR_ARG_VALUE: Final = 'Invalid type argument value for "{}"'
TYPEVAR_VARIANCE_DEF: Final = 'TypeVar "{}" may only be a literal bool'
TYPEVAR_BOUND_MUST_BE_TYPE: Final = 'TypeVar "bound" must be a type'
TYPEVAR_ARG_MUST_BE_TYPE: Final = '{} "{}" must be a type'
TYPEVAR_UNEXPECTED_ARGUMENT: Final = 'Unexpected argument to "TypeVar()"'
UNBOUND_TYPEVAR: Final = (
"A function returning TypeVar should receive at least "
Expand Down
151 changes: 123 additions & 28 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4106,28 +4106,17 @@ def process_typevar_parameters(
if has_values:
self.fail("TypeVar cannot have both values and an upper bound", context)
return None
try:
# We want to use our custom error message below, so we suppress
# the default error message for invalid types here.
analyzed = self.expr_to_analyzed_type(
param_value, allow_placeholder=True, report_invalid_types=False
)
if analyzed is None:
# Type variables are special: we need to place them in the symbol table
# soon, even if upper bound is not ready yet. Otherwise avoiding
# a "deadlock" in this common pattern would be tricky:
# T = TypeVar('T', bound=Custom[Any])
# class Custom(Generic[T]):
# ...
analyzed = PlaceholderType(None, [], context.line)
upper_bound = get_proper_type(analyzed)
if isinstance(upper_bound, AnyType) and upper_bound.is_from_error:
self.fail(message_registry.TYPEVAR_BOUND_MUST_BE_TYPE, param_value)
# Note: we do not return 'None' here -- we want to continue
# using the AnyType as the upper bound.
except TypeTranslationError:
self.fail(message_registry.TYPEVAR_BOUND_MUST_BE_TYPE, param_value)
tv_arg = self.get_typevarlike_argument("TypeVar", param_name, param_value, context)
if tv_arg is None:
return None
upper_bound = tv_arg
elif param_name == "default":
tv_arg = self.get_typevarlike_argument(
"TypeVar", param_name, param_value, context, allow_unbound_tvars=True
)
if tv_arg is None:
return None
default = tv_arg
elif param_name == "values":
# Probably using obsolete syntax with values=(...). Explain the current syntax.
self.fail('TypeVar "values" argument not supported', context)
Expand Down Expand Up @@ -4155,6 +4144,50 @@ def process_typevar_parameters(
variance = INVARIANT
return variance, upper_bound, default

def get_typevarlike_argument(
self,
typevarlike_name: str,
param_name: str,
param_value: Expression,
context: Context,
*,
allow_unbound_tvars: bool = False,
allow_param_spec_literals: bool = False,
) -> ProperType | None:
try:
# We want to use our custom error message below, so we suppress
# the default error message for invalid types here.
analyzed = self.expr_to_analyzed_type(
param_value,
allow_placeholder=True,
report_invalid_types=False,
allow_unbound_tvars=allow_unbound_tvars,
allow_param_spec_literals=allow_param_spec_literals,
)
if analyzed is None:
# Type variables are special: we need to place them in the symbol table
# soon, even if upper bound is not ready yet. Otherwise avoiding
# a "deadlock" in this common pattern would be tricky:
# T = TypeVar('T', bound=Custom[Any])
# class Custom(Generic[T]):
# ...
analyzed = PlaceholderType(None, [], context.line)
typ = get_proper_type(analyzed)
if isinstance(typ, AnyType) and typ.is_from_error:
self.fail(
message_registry.TYPEVAR_ARG_MUST_BE_TYPE.format(typevarlike_name, param_name),
param_value,
)
# Note: we do not return 'None' here -- we want to continue
# using the AnyType as the upper bound.
return typ
except TypeTranslationError:
self.fail(
message_registry.TYPEVAR_ARG_MUST_BE_TYPE.format(typevarlike_name, param_name),
param_value,
)
return None

def extract_typevarlike_name(self, s: AssignmentStmt, call: CallExpr) -> str | None:
if not call:
return None
Expand Down Expand Up @@ -4187,13 +4220,47 @@ def process_paramspec_declaration(self, s: AssignmentStmt) -> bool:
if name is None:
return False

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

default: Type = AnyType(TypeOfAny.from_omitted_generics)
for param_value, param_name in zip(
call.args[1 + n_values :], call.arg_names[1 + n_values :]
):
if param_name == "default":
tv_arg = self.get_typevarlike_argument(
"ParamSpec",
param_name,
param_value,
s,
allow_unbound_tvars=True,
allow_param_spec_literals=True,
)
if tv_arg is None:
return False
default = tv_arg
if isinstance(tv_arg, Parameters):
for i, arg_type in enumerate(tv_arg.arg_types):
typ = get_proper_type(arg_type)
if isinstance(typ, AnyType) and typ.is_from_error:
self.fail(
f"Argument {i} of ParamSpec default must be a type", param_value
)
elif not isinstance(default, (AnyType, UnboundType)):
self.fail(
"The default argument to ParamSpec must be a tuple expression, ellipsis, or a ParamSpec",
param_value,
)
default = AnyType(TypeOfAny.from_error)
else:
# ParamSpec is different from a regular TypeVar:
# arguments are not semantically valid. But, allowed in runtime.
# So, we need to warn users about possible invalid usage.
self.fail(
"The variance and bound arguments to ParamSpec do not have defined semantics yet",
s,
)

# PEP 612 reserves the right to define bound, covariant and contravariant arguments to
# ParamSpec in a later PEP. If and when that happens, we should do something
Expand Down Expand Up @@ -4227,10 +4294,34 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool:
if not call:
return False

if len(call.args) > 1:
self.fail("Only the first argument to TypeVarTuple has defined semantics", s)
n_values = call.arg_kinds[1:].count(ARG_POS)
if n_values != 0:
self.fail(
"Only the first positional argument to TypeVarTuple has defined semantics", s
)

default: Type = AnyType(TypeOfAny.from_omitted_generics)
for param_value, param_name in zip(
call.args[1 + n_values :], call.arg_names[1 + n_values :]
):
if param_name == "default":
tv_arg = self.get_typevarlike_argument(
"TypeVarTuple", param_name, param_value, s, allow_unbound_tvars=True
)
if tv_arg is None:
return False
default = tv_arg
if not isinstance(default, UnpackType):
self.fail(
"The default argument to TypeVarTuple must be an Unpacked tuple",
param_value,
)
default = AnyType(TypeOfAny.from_error)
else:
self.fail(
"The variance and bound arguments to TypeVarTuple do not have defined semantics yet",
s,
)

if not self.incomplete_feature_enabled(TYPE_VAR_TUPLE, s):
return False
Expand Down Expand Up @@ -6324,6 +6415,8 @@ def expr_to_analyzed_type(
report_invalid_types: bool = True,
allow_placeholder: bool = False,
allow_type_any: bool = False,
allow_unbound_tvars: bool = False,
allow_param_spec_literals: bool = False,
) -> Type | None:
if isinstance(expr, CallExpr):
# This is a legacy syntax intended mostly for Python 2, we keep it for
Expand Down Expand Up @@ -6352,6 +6445,8 @@ def expr_to_analyzed_type(
report_invalid_types=report_invalid_types,
allow_placeholder=allow_placeholder,
allow_type_any=allow_type_any,
allow_unbound_tvars=allow_unbound_tvars,
allow_param_spec_literals=allow_param_spec_literals,
)

def analyze_type_expr(self, expr: Expression) -> None:
Expand Down
7 changes: 5 additions & 2 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
TypedDictType,
TypeList,
TypeOfAny,
TypeOfTypeList,
TypeQuery,
TypeType,
TypeVarLikeType,
Expand Down Expand Up @@ -896,10 +897,12 @@ def visit_type_list(self, t: TypeList) -> Type:
else:
return AnyType(TypeOfAny.from_error)
else:
s = "[...]" if t.list_type == TypeOfTypeList.callable_args else "(...)"
self.fail(
'Bracketed expression "[...]" is not valid as a type', t, code=codes.VALID_TYPE
f'Bracketed expression "{s}" is not valid as a type', t, code=codes.VALID_TYPE
)
self.note('Did you mean "List[...]"?', t)
if t.list_type == TypeOfTypeList.callable_args:
self.note('Did you mean "List[...]"?', t)
return AnyType(TypeOfAny.from_error)

def visit_callable_argument(self, t: CallableArgument) -> Type:
Expand Down
50 changes: 43 additions & 7 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,17 @@ class TypeOfAny:
suggestion_engine: Final = 9


class TypeOfTypeList:
"""This class describes the different types of TypeList."""

__slots__ = ()

# List expressions for callable args
callable_args: Final = 1
# Tuple expressions for ParamSpec defaults
param_spec_defaults: Final = 2


def deserialize_type(data: JsonDict | str) -> Type:
if isinstance(data, str):
return Instance.deserialize(data)
Expand Down Expand Up @@ -994,13 +1005,20 @@ class TypeList(ProperType):
types before they are processed into Callable types.
"""

__slots__ = ("items",)
__slots__ = ("items", "list_type")

items: list[Type]

def __init__(self, items: list[Type], line: int = -1, column: int = -1) -> None:
def __init__(
self,
items: list[Type],
list_type: int = TypeOfTypeList.callable_args,
line: int = -1,
column: int = -1,
) -> None:
super().__init__(line, column)
self.items = items
self.list_type = list_type

def accept(self, visitor: TypeVisitor[T]) -> T:
assert isinstance(visitor, SyntheticTypeVisitor)
Expand All @@ -1014,7 +1032,11 @@ def __hash__(self) -> int:
return hash(tuple(self.items))

def __eq__(self, other: object) -> bool:
return isinstance(other, TypeList) and self.items == other.items
return (
isinstance(other, TypeList)
and self.items == other.items
and self.list_type == other.list_type
)


class UnpackType(ProperType):
Expand Down Expand Up @@ -3041,6 +3063,8 @@ def visit_type_var(self, t: TypeVarType) -> str:
s = f"{t.name}`{t.id}"
if self.id_mapper and t.upper_bound:
s += f"(upper_bound={t.upper_bound.accept(self)})"
if t.has_default():
s += f" = {t.default.accept(self)}"
return s

def visit_param_spec(self, t: ParamSpecType) -> str:
Expand All @@ -3056,6 +3080,8 @@ def visit_param_spec(self, t: ParamSpecType) -> str:
s += f"{t.name_with_suffix()}`{t.id}"
if t.prefix.arg_types:
s += "]"
if t.has_default():
s += f" = {t.default.accept(self)}"
return s

def visit_parameters(self, t: Parameters) -> str:
Expand Down Expand Up @@ -3094,6 +3120,8 @@ def visit_type_var_tuple(self, t: TypeVarTupleType) -> str:
else:
# Named type variable type.
s = f"{t.name}`{t.id}"
if t.has_default():
s += f" = {t.default.accept(self)}"
return s

def visit_callable_type(self, t: CallableType) -> str:
Expand Down Expand Up @@ -3130,6 +3158,8 @@ def visit_callable_type(self, t: CallableType) -> str:
if s:
s += ", "
s += f"*{n}.args, **{n}.kwargs"
if param_spec.has_default():
s += f" = {param_spec.default.accept(self)}"

s = f"({s})"

Expand All @@ -3148,12 +3178,18 @@ def visit_callable_type(self, t: CallableType) -> str:
vals = f"({', '.join(val.accept(self) for val in var.values)})"
vs.append(f"{var.name} in {vals}")
elif not is_named_instance(var.upper_bound, "builtins.object"):
vs.append(f"{var.name} <: {var.upper_bound.accept(self)}")
vs.append(
f"{var.name} <: {var.upper_bound.accept(self)}{f' = {var.default.accept(self)}' if var.has_default() else ''}"
)
else:
vs.append(var.name)
vs.append(
f"{var.name}{f' = {var.default.accept(self)}' if var.has_default() else ''}"
)
else:
# For other TypeVarLikeTypes, just use the name
vs.append(var.name)
# For other TypeVarLikeTypes, use the name and default
vs.append(
f"{var.name}{f' = {var.default.accept(self)}' if var.has_default() else ''}"
)
s = f"[{', '.join(vs)}] {s}"

return f"def {s}"
Expand Down
10 changes: 5 additions & 5 deletions test-data/unit/check-parameter-specification.test
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ P = ParamSpec('P')
[case testInvalidParamSpecDefinitions]
from typing import ParamSpec

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

[case testParamSpecLocations]
Expand Down
Loading

0 comments on commit 5c29737

Please sign in to comment.