diff --git a/mypy/exprtotype.py b/mypy/exprtotype.py index bbc284a5188a6..ca5adb6d186ae 100644 --- a/mypy/exprtotype.py +++ b/mypy/exprtotype.py @@ -33,6 +33,7 @@ Type, TypeList, TypeOfAny, + TypeOfTypeList, UnboundType, UnionType, ) @@ -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, ) diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 130b94c7bf9a1..64f709f176951 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -179,7 +179,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 " diff --git a/mypy/semanal.py b/mypy/semanal.py index a2d051dd6b1ac..1217d83f27490 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -4100,28 +4100,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) @@ -4149,6 +4138,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 @@ -4181,13 +4214,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 @@ -4215,10 +4282,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 @@ -6311,6 +6402,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 @@ -6339,6 +6432,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: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 7b1a6947d8415..ac34f9f80c09f 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -72,6 +72,7 @@ TypedDictType, TypeList, TypeOfAny, + TypeOfTypeList, TypeQuery, TypeType, TypeVarLikeType, @@ -890,10 +891,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: diff --git a/mypy/types.py b/mypy/types.py index 803067a4688b4..d521140b432c8 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -196,6 +196,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) @@ -990,13 +1001,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) @@ -1010,7 +1028,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): @@ -3036,6 +3058,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: @@ -3051,6 +3075,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: @@ -3089,6 +3115,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: @@ -3125,6 +3153,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})" @@ -3143,12 +3173,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}" diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index fe66b18fbfea6..b46e1f14d1eed 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -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] diff --git a/test-data/unit/check-typevar-defaults.test b/test-data/unit/check-typevar-defaults.test new file mode 100644 index 0000000000000..3a7e55920de8c --- /dev/null +++ b/test-data/unit/check-typevar-defaults.test @@ -0,0 +1,73 @@ +[case testTypeVarDefaultsBasic] +import builtins +from typing import Generic, TypeVar, ParamSpec, Callable, Tuple, List +from typing_extensions import TypeVarTuple, Unpack + +T1 = TypeVar("T1", default=int) +P1 = ParamSpec("P1", default=(int, str)) +Ts1 = TypeVarTuple("Ts1", default=Unpack[Tuple[int, str]]) + +def f1(a: T1) -> List[T1]: ... +reveal_type(f1) # N: Revealed type is "def [T1 = builtins.int] (a: T1`-1 = builtins.int) -> builtins.list[T1`-1 = builtins.int]" + +def f2(a: Callable[P1, None] ) -> Callable[P1, None]: ... +reveal_type(f2) # N: Revealed type is "def [P1 = [builtins.int, builtins.str]] (a: def (*P1.args, **P1.kwargs)) -> def (*P1.args, **P1.kwargs)" + +def f3(a: Tuple[Unpack[Ts1]]) -> Tuple[Unpack[Ts1]]: ... +reveal_type(f3) # N: Revealed type is "def [Ts1 = Unpack[Tuple[builtins.int, builtins.str]]] (a: Tuple[Unpack[Ts1`-1 = Unpack[Tuple[builtins.int, builtins.str]]]]) -> Tuple[Unpack[Ts1`-1 = Unpack[Tuple[builtins.int, builtins.str]]]]" + + +class ClassA1(Generic[T1]): ... +class ClassA2(Generic[P1]): ... +class ClassA3(Generic[Unpack[Ts1]]): ... + +reveal_type(ClassA1) # N: Revealed type is "def [T1 = builtins.int] () -> __main__.ClassA1[T1`1 = builtins.int]" +reveal_type(ClassA2) # N: Revealed type is "def [P1 = [builtins.int, builtins.str]] () -> __main__.ClassA2[P1`1 = [builtins.int, builtins.str]]" +reveal_type(ClassA3) # N: Revealed type is "def [Ts1 = Unpack[Tuple[builtins.int, builtins.str]]] () -> __main__.ClassA3[Unpack[Ts1`1 = Unpack[Tuple[builtins.int, builtins.str]]]]" +[builtins fixtures/tuple.pyi] + +[case testTypeVarDefaultsValid] +from typing import TypeVar, ParamSpec, Any, List, Tuple +from typing_extensions import TypeVarTuple, Unpack + +S0 = TypeVar("S0") +S1 = TypeVar("S1", bound=int) + +P0 = ParamSpec("P0") +Ts0 = TypeVarTuple("Ts0") + +T1 = TypeVar("T1", default=int) +T2 = TypeVar("T2", bound=float, default=int) +T3 = TypeVar("T3", bound=List[Any], default=List[int]) +T4 = TypeVar("T4", int, str, default=int) +T5 = TypeVar("T5", default=S0) +T6 = TypeVar("T6", bound=float, default=S1) +# T7 = TypeVar("T7", bound=List[Any], default=List[S0]) # TODO + +P1 = ParamSpec("P1", default=()) +P2 = ParamSpec("P2", default=...) +P3 = ParamSpec("P3", default=(int, str)) +P4 = ParamSpec("P4", default=P0) + +Ts1 = TypeVarTuple("Ts1", default=Unpack[Tuple[int]]) +Ts2 = TypeVarTuple("Ts2", default=Unpack[Tuple[int, ...]]) +# Ts3 = TypeVarTuple("Ts3", default=Unpack[Ts0]) # TODO +[builtins fixtures/tuple.pyi] + +[case testTypeVarDefaultsInvalid] +from typing import TypeVar, ParamSpec, Tuple +from typing_extensions import TypeVarTuple, Unpack + +T1 = TypeVar("T1", default=2) # E: TypeVar "default" must be a type +T2 = TypeVar("T2", default=(int, str)) # E: Bracketed expression "(...)" is not valid as a type \ + # E: TypeVar "default" must be a type + +P1 = ParamSpec("P1", default=int) # E: The default argument to ParamSpec must be a tuple expression, ellipsis, or a ParamSpec +P2 = ParamSpec("P2", default=2) # E: ParamSpec "default" must be a type +P3 = ParamSpec("P3", default=(2, int)) # E: Argument 0 of ParamSpec default must be a type + +Ts1 = TypeVarTuple("Ts1", default=2) # E: TypeVarTuple "default" must be a type \ + # E: The default argument to TypeVarTuple must be an Unpacked tuple +Ts2 = TypeVarTuple("Ts2", default=int) # E: The default argument to TypeVarTuple must be an Unpacked tuple +Ts3 = TypeVarTuple("Ts3", default=Tuple[int]) # E: The default argument to TypeVarTuple must be an Unpacked tuple +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/semanal-errors.test b/test-data/unit/semanal-errors.test index a4ed905dcb9f5..9d4945de90cd5 100644 --- a/test-data/unit/semanal-errors.test +++ b/test-data/unit/semanal-errors.test @@ -1045,7 +1045,7 @@ c = TypeVar(1) # E: TypeVar() expects a string literal as first argument T = TypeVar(b'T') # E: TypeVar() expects a string literal as first argument d = TypeVar('D') # E: String argument 1 "D" to TypeVar(...) does not match variable name "d" e = TypeVar('e', int, str, x=1) # E: Unexpected argument to "TypeVar()": "x" -f = TypeVar('f', (int, str), int) # E: Type expected +f = TypeVar('f', (int, str), int) # E: Bracketed expression "(...)" is not valid as a type g = TypeVar('g', int) # E: TypeVar cannot have only a single constraint h = TypeVar('h', x=(int, str)) # E: Unexpected argument to "TypeVar()": "x" i = TypeVar('i', bound=1) # E: TypeVar "bound" must be a type @@ -1465,7 +1465,7 @@ TVariadic2 = TypeVarTuple('TVariadic2') TP = TypeVarTuple('?') # E: String argument 1 "?" to TypeVarTuple(...) does not match variable name "TP" TP2: int = TypeVarTuple('TP2') # E: Cannot declare the type of a TypeVar or similar construct TP3 = TypeVarTuple() # E: Too few arguments for TypeVarTuple() -TP4 = TypeVarTuple('TP4', 'TP4') # E: Only the first argument to TypeVarTuple has defined semantics +TP4 = TypeVarTuple('TP4', 'TP4') # E: Only the first positional argument to TypeVarTuple has defined semantics TP5 = TypeVarTuple(t='TP5') # E: TypeVarTuple() expects a string literal as first argument x: TVariadic # E: TypeVarTuple "TVariadic" is unbound