8000 Allow Ellipsis in Concatenate; cleanup ParamSpec literals (#15905) · python/mypy@1db3eb3 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1db3eb3

Browse files
authored
Allow Ellipsis in Concatenate; cleanup ParamSpec literals (#15905)
Fixes #14761 Fixes #15318 Fixes #14656 Fixes #13518 I noticed there is a bunch of inconsistencies in `semanal`/`typeanal` for ParamSpecs, so I decided do a small cleanup. Using this opportunity I also allow `Concatenate[int, ...]` (with literal Ellipsis), and reduce verbosity of some errors. cc @A5rocks
1 parent b02ddf1 commit 1db3eb3

6 files changed

+123
-33
lines changed

mypy/semanal.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5285,20 +5285,18 @@ def analyze_type_application_args(self, expr: IndexExpr) -> list[Type] | None:
52855285
else:
52865286
items = [index]
52875287

5288-
# whether param spec literals be allowed here
5289-
# TODO: should this be computed once and passed in?
5290-
# or is there a better way to do this?
5288+
# TODO: this needs a clean-up.
5289+
# Probably always allow Parameters literals, and validate in semanal_typeargs.py
52915290
base = expr.base
52925291
if isinstance(base, RefExpr) and isinstance(base.node, TypeAlias):
52935292
alias = base.node
5294-
target = get_proper_type(alias.target)
5295-
if isinstance(target, Instance):
5296-
has_param_spec = target.type.has_param_spec_type
5297-
num_args = len(target.type.type_vars)
5293+
if any(isinstance(t, ParamSpecType) for t in alias.alias_tvars):
5294+
has_param_spec = True
5295+
num_args = len(alias.alias_tvars)
52985296
else:
52995297
has_param_spec = False
53005298
num_args = -1
5301-
elif isinstance(base, NameExpr) and isinstance(base.node, TypeInfo):
5299+
elif isinstance(base, RefExpr) and isinstance(base.node, TypeInfo):
53025300
has_param_spec = base.node.has_param_spec_type
53035301
num_args = len(base.node.type_vars)
53045302
else:

mypy/typeanal.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ def __init__(
226226
self.allow_required = allow_required
227227
# Are we in a context where ParamSpec literals are allowed?
228228
self.allow_param_spec_literals = allow_param_spec_literals
229+
# Are we in context where literal "..." specifically is allowed?
230+
self.allow_ellipsis = False
229231
# Should we report an error whenever we encounter a RawExpressionType outside
230232
# of a Literal context: e.g. whenever we encounter an invalid type? Normally,
231233
# 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:
461463
self.api.fail("Concatenate needs type arguments", t, code=codes.VALID_TYPE)
462464
return AnyType(TypeOfAny.from_error)
463465

464-
# last argument has to be ParamSpec
465-
ps = self.anal_type(t.args[-1], allow_param_spec=True)
466-
if not isinstance(ps, ParamSpecType):
466+
# Last argument has to be ParamSpec or Ellipsis.
467+
ps = self.anal_type(t.args[-1], allow_param_spec=True, allow_ellipsis=True)
468+
if not isinstance(ps, (ParamSpecType, Parameters)):
467469
if isinstance(ps, UnboundType) and self.allow_unbound_tvars:
468470
sym = self.lookup_qualified(ps.name, t)
469471
if sym is not None and isinstance(sym.node, ParamSpecExpr):
@@ -477,19 +479,19 @@ def apply_concatenate_operator(self, t: UnboundType) -> Type:
477479

478480
# TODO: this may not work well with aliases, if those worked.
479481
# Those should be special-cased.
480-
elif ps.prefix.arg_types:
482+
elif isinstance(ps, ParamSpecType) and ps.prefix.arg_types:
481483
self.api.fail("Nested Concatenates are invalid", t, code=codes.VALID_TYPE)
482484

483485
args = self.anal_array(t.args[:-1])
484-
pre = ps.prefix
486+
pre = ps.prefix if isinstance(ps, ParamSpecType) else ps
485487

486488
# mypy can't infer this :(
487489
names: list[str | None] = [None] * len(args)
488490

489491
pre = Parameters(
490492
args + pre.arg_types, [ARG_POS] * len(args) + pre.arg_kinds, names + pre.arg_names
491493
)
492-
return ps.copy_modified(prefix=pre)
494+
return ps.copy_modified(prefix=pre) if isinstance(ps, ParamSpecType) else pre
493495

494496
def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Type | None:
495497
"""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:
880882
return t
881883

882884
def visit_type_list(self, t: TypeList) -> Type:
883-
# paramspec literal (Z[[int, str, Whatever]])
885+
# Parameters literal (Z[[int, str, Whatever]])
884886
if self.allow_param_spec_literals:
885887
params = self.analyze_callable_args(t)
886888
if params:
@@ -893,7 +895,8 @@ def visit_type_list(self, t: TypeList) -> Type:
893895
self.fail(
894896
'Bracketed expression "[...]" is not valid as a type', t, code=codes.VALID_TYPE
895897
)
896-
self.note('Did you mean "List[...]"?', t)
898+
if len(t.items) == 1:
899+
self.note('Did you mean "List[...]"?', t)
897900
return AnyType(TypeOfAny.from_error)
898901

899902
def visit_callable_argument(self, t: CallableArgument) -> Type:
@@ -1106,7 +1109,7 @@ def visit_partial_type(self, t: PartialType) -> Type:
11061109
assert False, "Internal error: Unexpected partial type"
11071110

11081111
def visit_ellipsis_type(self, t: EllipsisType) -> Type:
1109-
if self.allow_param_spec_literals:
1112+
if self.allow_ellipsis or self.allow_param_spec_literals:
11101113
any_type = AnyType(TypeOfAny.explicit)
11111114
return Parameters(
11121115
[any_type, any_type], [ARG_STAR, ARG_STAR2], [None, None], is_ellipsis_args=True
@@ -1174,7 +1177,7 @@ def analyze_callable_args_for_paramspec(
11741177

11751178
def analyze_callable_args_for_concatenate(
11761179
self, callable_args: Type, ret_type: Type, fallback: Instance
1177-
) -> CallableType | None:
1180+
) -> CallableType | AnyType | None:
11781181
"""Construct a 'Callable[C, RET]', where C is Concatenate[..., P], returning None if we
11791182
cannot.
11801183
"""
@@ -1189,7 +1192,7 @@ def analyze_callable_args_for_concatenate(
11891192
return None
11901193

11911194
tvar_def = self.anal_type(callable_args, allow_param_spec=True)
1192-
if not isinstance(tvar_def, ParamSpecType):
1195+
if not isinstance(tvar_def, (ParamSpecType, Parameters)):
11931196
if self.allow_unbound_tvars and isinstance(tvar_def, UnboundType):
11941197
sym = self.lookup_qualified(tvar_def.name, callable_args)
11951198
if sym is not None and isinstance(sym.node, ParamSpecExpr):
@@ -1198,7 +1201,18 @@ def analyze_callable_args_for_concatenate(
11981201
return callable_with_ellipsis(
11991202
AnyType(TypeOfAny.explicit), ret_type=ret_type, fallback=fallback
12001203
)
1201-
return None
1204+
# Error was already given, so prevent further errors.
1205+
return AnyType(TypeOfAny.from_error)
1206+
if isinstance(tvar_def, Parameters):
1207+
# This comes from Concatenate[int, ...]
1208+
return CallableType(
1209+
arg_types=tvar_def.arg_types,
1210+
arg_names=tvar_def.arg_names,
1211+
arg_kinds=tvar_def.arg_kinds,
1212+
ret_type=ret_type,
1213+
fallback=fallback,
1214+
from_concatenate=True,
1215+
)
12021216

12031217
# ick, CallableType should take ParamSpecType
12041218
prefix = tvar_def.prefix
@@ -1257,7 +1271,7 @@ def analyze_callable_type(self, t: UnboundType) -> Type:
12571271
) or self.analyze_callable_args_for_concatenate(
12581272
callable_args, ret_type, fallback
12591273
)
1260-
if maybe_ret:
1274+
if isinstance(maybe_ret, CallableType):
12611275
maybe_ret = maybe_ret.copy_modified(
12621276
ret_type=ret_type.accept(self), variables=variables
12631277
)
@@ -1274,6 +1288,8 @@ def analyze_callable_type(self, t: UnboundType) -> Type:
12741288
t,
12751289
)
12761290
return AnyType(TypeOfAny.from_error)
1291+
elif isinstance(maybe_ret, AnyType):
1292+
return maybe_ret
12771293
ret = maybe_ret
12781294
else:
12791295
if self.options.disallow_any_generics:
@@ -1527,17 +1543,27 @@ def anal_array(
15271543
self.allow_param_spec_literals = old_allow_param_spec_literals
15281544
return self.check_unpacks_in_list(res)
15291545

1530-
def anal_type(self, t: Type, nested: bool = True, *, allow_param_spec: bool = False) -> Type:
1546+
def anal_type(
1547+
self,
1548+
t: Type,
1549+
nested: bool = True,
1550+
*,
1551+
allow_param_spec: bool = False,
1552+
allow_ellipsis: bool = False,
1553+
) -> Type:
15311554
if nested:
15321555
self.nesting_level += 1
15331556
old_allow_required = self.allow_required
15341557
self.allow_required = False
< F987 /code>1558+
old_allow_ellipsis = self.allow_ellipsis
1559+
self.allow_ellipsis = allow_ellipsis
15351560
try:
15361561
analyzed = t.accept(self)
15371562
finally:
15381563
if nested:
15391564
self.nesting_level -= 1
15401565
self.allow_required = old_allow_required
1566+
self.allow_ellipsis = old_allow_ellipsis
15411567
if (
15421568
not allow_param_spec
15431569
and isinstance(analyzed, ParamSpecType)

test-data/unit/check-literal.test

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -611,8 +611,7 @@ from typing_extensions import Literal
611611
a: (1, 2, 3) # E: Syntax error in type annotation \
612612
# N: Suggestion: Use Tuple[T1, ..., Tn] instead of (T1, ..., Tn)
613613
b: Literal[[1, 2, 3]] # E: Parameter 1 of Literal[...] is invalid
614-
c: [1, 2, 3] # E: Bracketed expression "[...]" is not valid as a type \
615-
# N: Did you mean "List[...]"?
614+
c: [1, 2, 3] # E: Bracketed expression "[...]" is not valid as a type
616615
[builtins fixtures/tuple.pyi]
617616
[out]
618617

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

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,74 @@ def foo6(x: Callable[[P], int]) -> None: ... # E: Invalid location for ParamSpe
3838
# N: You can use ParamSpec as the first argument to Callable, e.g., 'Callable[P, int]'
3939
[builtins fixtures/paramspec.pyi]
4040

41+
[case testParamSpecImports]
42+
import lib
43+
from lib import Base
44+
45+
class C(Base[[int]]):
46+
def test(self, x: int): ...
47+
48+
class D(lib.Base[[int]]):
49+
def test(self, x: int): ...
50+
51+
class E(lib.Base[...]): ...
52+
reveal_type(E().test) # N: Revealed type is "def (*Any, **Any)"
53+
54+
[file lib.py]
55+
from typing import Generic
56+
from typing_extensions import ParamSpec
57+
58+
P = ParamSpec("P")
59+
class Base(Generic[P]):
60+
def test(self, *args: P.args, **kwargs: P.kwargs) -> None:
61+
...
62+
[builtins fixtures/paramspec.pyi]
63+
64+
[case testParamSpecEllipsisInAliases]
65+
from typing import Any, Callable, Generic, TypeVar
66+
from typing_extensions import ParamSpec
67+
68+
P = ParamSpec('P')
69+
R = TypeVar('R')
70+
Alias = Callable[P, R]
71+
72+
class B(Generic[P]): ...
73+
Other = B[P]
74+
75+
T = TypeVar('T', bound=Alias[..., Any])
76+
Alias[..., Any] # E: Type application is only supported for generic classes
77+
B[...]
78+
Other[...]
79+
[builtins fixtures/paramspec.pyi]
80+
81+
[case testParamSpecEllipsisInConcatenate]
82+
from typing import Any, Callable, Generic, TypeVar
83+
from typing_extensions import ParamSpec, Concatenate
84+
85+
P = ParamSpec('P')
86+
R = TypeVar('R')
87+
Alias = Callable[P, R]
88+
89+
IntFun = Callable[Concatenate[int, ...], None]
90+
f: IntFun
91+
reveal_type(f) # N: Revealed type is "def (builtins.int, *Any, **Any)"
92+
93+
g: Callable[Concatenate[int, ...], None]
94+
reveal_type(g) # N: Revealed type is "def (builtins.int, *Any, **Any)"
95+
96+
class B(Generic[P]):
97+
def test(self, *args: P.args, **kwargs: P.kwargs) -> None:
98+
...
99+
100+
x: B[Concatenate[int, ...]]
101+
reveal_type(x.test) # N: Revealed type is "def (builtins.int, *Any, **Any)"
102+
103+
Bad = Callable[Concatenate[int, [int, str]], None] # E: The last parameter to Concatenate needs to be a ParamSpec \
104+
# E: Bracketed expression "[...]" is not valid as a type
105+
def bad(fn: Callable[Concatenate[P, int], None]): # E: The last parameter to Concatenate needs to be a ParamSpec
106+
...
107+
[builtins fixtures/paramspec.pyi]
108+
41109
[case testParamSpecContextManagerLike]
42110
from typing import Callable, List, Iterator, TypeVar
43111
from typing_extensions import ParamSpec
@@ -1431,8 +1499,7 @@ from typing import ParamSpec, Generic, List, TypeVar, Callable
14311499
P = ParamSpec("P")
14321500
T = TypeVar("T")
14331501
A = List[T]
1434-
def f(x: A[[int, str]]) -> None: ... # E: Bracketed expression "[...]" is not valid as a type \
1435-
# N: Did you mean "List[...]"?
1502+
def f(x: A[[int, str]]) -> None: ... # E: Bracketed expression "[...]" is not valid as a type
14361503
def g(x: A[P]) -> None: ... # E: Invalid location for ParamSpec "P" \
14371504
# N: You can use ParamSpec as the first argument to Callable, e.g., 'Callable[P, int]'
14381505

test-data/unit/check-typevar-defaults.test

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ from typing import TypeVar, ParamSpec, Tuple
5959
from typing_extensions import TypeVarTuple, Unpack
6060

6161
T1 = TypeVar("T1", default=2) # E: TypeVar "default" must be a type
62-
T2 = TypeVar("T2", default=[int, str]) # E: Bracketed expression "[...]" is not valid as a type \
63-
# N: Did you mean "List[...]"? \
64-
# E: TypeVar "default" must be a type
62+
T2 = TypeVar("T2", default=[int]) # E: Bracketed expression "[...]" is not valid as a type \
63+
# N: Did you mean "List[...]"? \
64+
# E: TypeVar "default" must be a type
6565

6666
P1 = ParamSpec("P1", default=int) # E: The default argument to ParamSpec must be a list expression, ellipsis, or a ParamSpec
6767
P2 = ParamSpec("P2", default=2) # E: The default argument to ParamSpec must be a list expression, ellipsis, or a ParamSpec

test-data/unit/semanal-errors.test

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -810,8 +810,8 @@ class C(Generic[t]): pass
810810
cast(str + str, None) # E: Cast target is not a type
811811
cast(C[str][str], None) # E: Cast target is not a type
812812
cast(C[str + str], None) # E: Cast target is not a type
813-
cast([int, str], None) # E: Bracketed expression "[...]" is not valid as a type \
814-
# N: Did you mean "List[...]"?
813+
cast([int], None) # E: Bracketed expression "[...]" is not valid as a type \
814+
# N: Did you mean "List[...]"?
815815
[out]
816816

817817
[case testInvalidCastTargetType]
@@ -859,8 +859,8 @@ Any(arg=str) # E: Any(...) is no longer supported. Use cast(Any, ...) instead
859859

860860
[case testTypeListAsType]
861861

862-
def f(x:[int, str]) -> None: # E: Bracketed expression "[...]" is not valid as a type \
863-
# N: Did you mean "List[...]"?
862+
def f(x: [int]) -> None: # E: Bracketed expression "[...]" is not valid as a type \
863+
# N: Did you mean "List[...]"?
864864
pass
865865
[out]
866866

0 commit comments

Comments
 (0)
0