diff --git a/mypy/semanal.py b/mypy/semanal.py index e21fc9f1c23f..5f33c9bf0c17 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -5276,20 +5276,18 @@ def analyze_type_application_args(self, expr: IndexExpr) -> list[Type] | None: else: items = [index] - # whether param spec literals be allowed here - # TODO: should this be computed once and passed in? - # or is there a better way to do this? + # TODO: this needs a clean-up. + # Probably always allow Parameters literals, and validate in semanal_typeargs.py base = expr.base if isinstance(base, RefExpr) and isinstance(base.node, TypeAlias): alias = base.node - target = get_proper_type(alias.target) - if isinstance(target, Instance): - has_param_spec = target.type.has_param_spec_type - num_args = len(target.type.type_vars) + if any(isinstance(t, ParamSpecType) for t in alias.alias_tvars): + has_param_spec = True + num_args = len(alias.alias_tvars) else: has_param_spec = False num_args = -1 - elif isinstance(base, NameExpr) and isinstance(base.node, TypeInfo): + elif isinstance(base, RefExpr) and isinstance(base.node, TypeInfo): has_param_spec = base.node.has_param_spec_type num_args = len(base.node.type_vars) else: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 8ac73cdf8aac..b15b5c7654ba 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -226,6 +226,8 @@ def __init__( self.allow_required = allow_required # Are we in a context where ParamSpec literals are allowed? self.allow_param_spec_literals = allow_param_spec_literals + # Are we in context where literal "..." specifically is allowed? + self.allow_ellipsis = False # Should we report an error whenever we encounter a RawExpressionType outside # of a Literal context: e.g. whenever we encounter an invalid type? Normally, # we want to report an error, but the caller may want to do more specialized @@ -461,9 +463,9 @@ def apply_concatenate_operator(self, t: UnboundType) -> Type: self.api.fail("Concatenate needs type arguments", t, code=codes.VALID_TYPE) return AnyType(TypeOfAny.from_error) - # last argument has to be ParamSpec - ps = self.anal_type(t.args[-1], allow_param_spec=True) - if not isinstance(ps, ParamSpecType): + # Last argument has to be ParamSpec or Ellipsis. + ps = self.anal_type(t.args[-1], allow_param_spec=True, allow_ellipsis=True) + if not isinstance(ps, (ParamSpecType, Parameters)): if isinstance(ps, UnboundType) and self.allow_unbound_tvars: sym = self.lookup_qualified(ps.name, t) if sym is not None and isinstance(sym.node, ParamSpecExpr): @@ -477,11 +479,11 @@ def apply_concatenate_operator(self, t: UnboundType) -> Type: # TODO: this may not work well with aliases, if those worked. # Those should be special-cased. - elif ps.prefix.arg_types: + elif isinstance(ps, ParamSpecType) and ps.prefix.arg_types: self.api.fail("Nested Concatenates are invalid", t, code=codes.VALID_TYPE) args = self.anal_array(t.args[:-1]) - pre = ps.prefix + pre = ps.prefix if isinstance(ps, ParamSpecType) else ps # mypy can't infer this :( names: list[str | None] = [None] * len(args) @@ -489,7 +491,7 @@ def apply_concatenate_operator(self, t: UnboundType) -> Type: pre = Parameters( args + pre.arg_types, [ARG_POS] * len(args) + pre.arg_kinds, names + pre.arg_names ) - return ps.copy_modified(prefix=pre) + return ps.copy_modified(prefix=pre) if isinstance(ps, ParamSpecType) else pre def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Type | None: """Bind special type that is recognized through magic name such as 'typing.Any'. @@ -880,7 +882,7 @@ def visit_deleted_type(self, t: DeletedType) -> Type: return t def visit_type_list(self, t: TypeList) -> Type: - # paramspec literal (Z[[int, str, Whatever]]) + # Parameters literal (Z[[int, str, Whatever]]) if self.allow_param_spec_literals: params = self.analyze_callable_args(t) if params: @@ -893,7 +895,8 @@ def visit_type_list(self, t: TypeList) -> Type: self.fail( 'Bracketed expression "[...]" is not valid as a type', t, code=codes.VALID_TYPE ) - self.note('Did you mean "List[...]"?', t) + if len(t.items) == 1: + self.note('Did you mean "List[...]"?', t) return AnyType(TypeOfAny.from_error) def visit_callable_argument(self, t: CallableArgument) -> Type: @@ -1106,7 +1109,7 @@ def visit_partial_type(self, t: PartialType) -> Type: assert False, "Internal error: Unexpected partial type" def visit_ellipsis_type(self, t: EllipsisType) -> Type: - if self.allow_param_spec_literals: + if self.allow_ellipsis or self.allow_param_spec_literals: any_type = AnyType(TypeOfAny.explicit) return Parameters( [any_type, any_type], [ARG_STAR, ARG_STAR2], [None, None], is_ellipsis_args=True @@ -1174,7 +1177,7 @@ def analyze_callable_args_for_paramspec( def analyze_callable_args_for_concatenate( self, callable_args: Type, ret_type: Type, fallback: Instance - ) -> CallableType | None: + ) -> CallableType | AnyType | None: """Construct a 'Callable[C, RET]', where C is Concatenate[..., P], returning None if we cannot. """ @@ -1189,7 +1192,7 @@ def analyze_callable_args_for_concatenate( return None tvar_def = self.anal_type(callable_args, allow_param_spec=True) - if not isinstance(tvar_def, ParamSpecType): + if not isinstance(tvar_def, (ParamSpecType, Parameters)): if self.allow_unbound_tvars and isinstance(tvar_def, UnboundType): sym = self.lookup_qualified(tvar_def.name, callable_args) if sym is not None and isinstance(sym.node, ParamSpecExpr): @@ -1198,7 +1201,18 @@ def analyze_callable_args_for_concatenate( return callable_with_ellipsis( AnyType(TypeOfAny.explicit), ret_type=ret_type, fallback=fallback ) - return None + # Error was already given, so prevent further errors. + return AnyType(TypeOfAny.from_error) + if isinstance(tvar_def, Parameters): + # This comes from Concatenate[int, ...] + return CallableType( + arg_types=tvar_def.arg_types, + arg_names=tvar_def.arg_names, + arg_kinds=tvar_def.arg_kinds, + ret_type=ret_type, + fallback=fallback, + from_concatenate=True, + ) # ick, CallableType should take ParamSpecType prefix = tvar_def.prefix @@ -1257,7 +1271,7 @@ def analyze_callable_type(self, t: UnboundType) -> Type: ) or self.analyze_callable_args_for_concatenate( callable_args, ret_type, fallback ) - if maybe_ret: + if isinstance(maybe_ret, CallableType): maybe_ret = maybe_ret.copy_modified( ret_type=ret_type.accept(self), variables=variables ) @@ -1274,6 +1288,8 @@ def analyze_callable_type(self, t: UnboundType) -> Type: t, ) return AnyType(TypeOfAny.from_error) + elif isinstance(maybe_ret, AnyType): + return maybe_ret ret = maybe_ret else: if self.options.disallow_any_generics: @@ -1527,17 +1543,27 @@ def anal_array( self.allow_param_spec_literals = old_allow_param_spec_literals return self.check_unpacks_in_list(res) - def anal_type(self, t: Type, nested: bool = True, *, allow_param_spec: bool = False) -> Type: + def anal_type( + self, + t: Type, + nested: bool = True, + *, + allow_param_spec: bool = False, + allow_ellipsis: bool = False, + ) -> Type: if nested: self.nesting_level += 1 old_allow_required = self.allow_required self.allow_required = False + old_allow_ellipsis = self.allow_ellipsis + self.allow_ellipsis = allow_ellipsis try: analyzed = t.accept(self) finally: if nested: self.nesting_level -= 1 self.allow_required = old_allow_required + self.allow_ellipsis = old_allow_ellipsis if ( not allow_param_spec and isinstance(analyzed, ParamSpecType) diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index 4498b2ddc9cf..ecd4fc0a1f00 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -611,8 +611,7 @@ from typing_extensions import Literal a: (1, 2, 3) # E: Syntax error in type annotation \ # N: Suggestion: Use Tuple[T1, ..., Tn] instead of (T1, ..., Tn) b: Literal[[1, 2, 3]] # E: Parameter 1 of Literal[...] is invalid -c: [1, 2, 3] # E: Bracketed expression "[...]" is not valid as a type \ - # N: Did you mean "List[...]"? +c: [1, 2, 3] # E: Bracketed expression "[...]" is not valid as a type [builtins fixtures/tuple.pyi] [out] diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index b06944389623..67fa24a0af3e 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -38,6 +38,74 @@ def foo6(x: Callable[[P], int]) -> None: ... # E: Invalid location for ParamSpe # N: You can use ParamSpec as the first argument to Callable, e.g., 'Callable[P, int]' [builtins fixtures/paramspec.pyi] +[case testParamSpecImports] +import lib +from lib import Base + +class C(Base[[int]]): + def test(self, x: int): ... + +class D(lib.Base[[int]]): + def test(self, x: int): ... + +class E(lib.Base[...]): ... +reveal_type(E().test) # N: Revealed type is "def (*Any, **Any)" + +[file lib.py] +from typing import Generic +from typing_extensions import ParamSpec + +P = ParamSpec("P") +class Base(Generic[P]): + def test(self, *args: P.args, **kwargs: P.kwargs) -> None: + ... +[builtins fixtures/paramspec.pyi] + +[case testParamSpecEllipsisInAliases] +from typing import Any, Callable, Generic, TypeVar +from typing_extensions import ParamSpec + +P = ParamSpec('P') +R = TypeVar('R') +Alias = Callable[P, R] + +class B(Generic[P]): ... +Other = B[P] + +T = TypeVar('T', bound=Alias[..., Any]) +Alias[..., Any] # E: Type application is only supported for generic classes +B[...] +Other[...] +[builtins fixtures/paramspec.pyi] + +[case testParamSpecEllipsisInConcatenate] +from typing import Any, Callable, Generic, TypeVar +from typing_extensions import ParamSpec, Concatenate + +P = ParamSpec('P') +R = TypeVar('R') +Alias = Callable[P, R] + +IntFun = Callable[Concatenate[int, ...], None] +f: IntFun +reveal_type(f) # N: Revealed type is "def (builtins.int, *Any, **Any)" + +g: Callable[Concatenate[int, ...], None] +reveal_type(g) # N: Revealed type is "def (builtins.int, *Any, **Any)" + +class B(Generic[P]): + def test(self, *args: P.args, **kwargs: P.kwargs) -> None: + ... + +x: B[Concatenate[int, ...]] +reveal_type(x.test) # N: Revealed type is "def (builtins.int, *Any, **Any)" + +Bad = Callable[Concatenate[int, [int, str]], None] # E: The last parameter to Concatenate needs to be a ParamSpec \ + # E: Bracketed expression "[...]" is not valid as a type +def bad(fn: Callable[Concatenate[P, int], None]): # E: The last parameter to Concatenate needs to be a ParamSpec + ... +[builtins fixtures/paramspec.pyi] + [case testParamSpecContextManagerLike] from typing import Callable, List, Iterator, TypeVar from typing_extensions import ParamSpec @@ -1431,8 +1499,7 @@ from typing import ParamSpec, Generic, List, TypeVar, Callable P = ParamSpec("P") T = TypeVar("T") A = List[T] -def f(x: A[[int, str]]) -> None: ... # E: Bracketed expression "[...]" is not valid as a type \ - # N: Did you mean "List[...]"? +def f(x: A[[int, str]]) -> None: ... # E: Bracketed expression "[...]" is not valid as a type def g(x: A[P]) -> None: ... # E: Invalid location for ParamSpec "P" \ # N: You can use ParamSpec as the first argument to Callable, e.g., 'Callable[P, int]' diff --git a/test-data/unit/check-typevar-defaults.test b/test-data/unit/check-typevar-defaults.test index 36ec125eb1a4..9015d353fa08 100644 --- a/test-data/unit/check-typevar-defaults.test +++ b/test-data/unit/check-typevar-defaults.test @@ -59,9 +59,9 @@ 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 \ - # N: Did you mean "List[...]"? \ - # E: TypeVar "default" must be a type +T2 = TypeVar("T2", default=[int]) # E: Bracketed expression "[...]" is not valid as a type \ + # N: Did you mean "List[...]"? \ + # E: TypeVar "default" must be a type P1 = ParamSpec("P1", default=int) # E: The default argument to ParamSpec must be a list expression, ellipsis, or a ParamSpec P2 = ParamSpec("P2", default=2) # E: The default argument to ParamSpec must be a list expression, ellipsis, or a ParamSpec diff --git a/test-data/unit/semanal-errors.test b/test-data/unit/semanal-errors.test index a098dd8791d4..09d4da54bff3 100644 --- a/test-data/unit/semanal-errors.test +++ b/test-data/unit/semanal-errors.test @@ -810,8 +810,8 @@ class C(Generic[t]): pass cast(str + str, None) # E: Cast target is not a type cast(C[str][str], None) # E: Cast target is not a type cast(C[str + str], None) # E: Cast target is not a type -cast([int, str], None) # E: Bracketed expression "[...]" is not valid as a type \ - # N: Did you mean "List[...]"? +cast([int], None) # E: Bracketed expression "[...]" is not valid as a type \ + # N: Did you mean "List[...]"? [out] [case testInvalidCastTargetType] @@ -859,8 +859,8 @@ Any(arg=str) # E: Any(...) is no longer supported. Use cast(Any, ...) instead [case testTypeListAsType] -def f(x:[int, str]) -> None: # E: Bracketed expression "[...]" is not valid as a type \ - # N: Did you mean "List[...]"? +def f(x: [int]) -> None: # E: Bracketed expression "[...]" is not valid as a type \ + # N: Did you mean "List[...]"? pass [out]