Skip to content

Commit

Permalink
Allow Ellipsis in Concatenate; cleanup ParamSpec literals (#15905)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ilevkivskyi committed Aug 19, 2023
1 parent b02ddf1 commit 1db3eb3
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 33 deletions.
14 changes: 6 additions & 8 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -5285,20 +5285,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:
Expand Down
54 changes: 40 additions & 14 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -477,19 +479,19 @@ 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)

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'.
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
"""
Expand All @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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
)
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions test-data/unit/check-literal.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
71 changes: 69 additions & 2 deletions test-data/unit/check-parameter-specification.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]'

Expand Down
6 changes: 3 additions & 3 deletions test-data/unit/check-typevar-defaults.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions test-data/unit/semanal-errors.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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]

Expand Down

0 comments on commit 1db3eb3

Please sign in to comment.