Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Ellipsis in Concatenate; cleanup ParamSpec literals #15905

Merged
merged 1 commit into from
Aug 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
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