Skip to content

Handle more ParamSpec literal form special cases #15256

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
has_any_from_unimported_type,
instantiate_type_alias,
make_optional_type,
pack_paramspec_args,
set_any_tvars,
)
from mypy.typeops import (
Expand Down Expand Up @@ -4083,6 +4084,9 @@ def apply_type_arguments_to_callable(
tp = get_proper_type(tp)

if isinstance(tp, CallableType):
if len(tp.variables) == 1 and isinstance(tp.variables[0], ParamSpecType):
args = pack_paramspec_args(args)

if len(tp.variables) != len(args):
if tp.is_type_obj() and tp.type_object().fullname == "builtins.tuple":
# TODO: Specialize the callable for the type arguments
Expand Down
38 changes: 25 additions & 13 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,29 @@ def no_subscript_builtin_alias(name: str, propose_alt: bool = True) -> str:
return msg


def pack_paramspec_args(an_args: Sequence[Type]) -> list[Type]:
# "Aesthetic" ParamSpec literals for single ParamSpec: C[int, str] -> C[[int, str]].
# These do not support mypy_extensions VarArgs, etc. as they were already analyzed
# TODO: should these be re-analyzed to get rid of this inconsistency?
count = len(an_args)
if count > 0:
first_arg = get_proper_type(an_args[0])
if not (count == 1 and isinstance(first_arg, (Parameters, ParamSpecType))):
if isinstance(first_arg, AnyType) and first_arg.type_of_any == TypeOfAny.from_error:
# Question: should I make new AnyTypes instead of reusing them?
return [
Parameters(
[first_arg, first_arg],
[ARG_STAR, ARG_STAR2],
[None, None],
is_ellipsis_args=True,
)
]
else:
return [Parameters(an_args, [ARG_POS] * count, [None] * count)]
return list(an_args)


class TypeAnalyser(SyntheticTypeVisitor[Type], TypeAnalyzerPluginInterface):
"""Semantic analyzer for types.

Expand Down Expand Up @@ -395,7 +418,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
allow_param_spec_literals=node.has_param_spec_type,
)
if node.has_param_spec_type and len(node.alias_tvars) == 1:
an_args = self.pack_paramspec_args(an_args)
an_args = pack_paramspec_args(an_args)

disallow_any = self.options.disallow_any_generics and not self.is_typeshed_stub
res = instantiate_type_alias(
Expand Down Expand Up @@ -440,17 +463,6 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
else: # sym is None
return AnyType(TypeOfAny.special_form)

def pack_paramspec_args(self, an_args: Sequence[Type]) -> list[Type]:
# "Aesthetic" ParamSpec literals for single ParamSpec: C[int, str] -> C[[int, str]].
# These do not support mypy_extensions VarArgs, etc. as they were already analyzed
# TODO: should these be re-analyzed to get rid of this inconsistency?
count = len(an_args)
if count > 0:
first_arg = get_proper_type(an_args[0])
if not (count == 1 and isinstance(first_arg, (Parameters, ParamSpecType, AnyType))):
return [Parameters(an_args, [ARG_POS] * count, [None] * count)]
return list(an_args)

def cannot_resolve_type(self, t: UnboundType) -> None:
# TODO: Move error message generation to messages.py. We'd first
# need access to MessageBuilder here. Also move the similar
Expand Down Expand Up @@ -686,7 +698,7 @@ def analyze_type_with_type_info(
ctx.column,
)
if len(info.type_vars) == 1 and info.has_param_spec_type:
instance.args = tuple(self.pack_paramspec_args(instance.args))
instance.args = tuple(pack_paramspec_args(instance.args))

if info.has_type_var_tuple_type:
if instance.args:
Expand Down
22 changes: 19 additions & 3 deletions test-data/unit/check-parameter-specification.test
Original file line number Diff line number Diff line change
Expand Up @@ -325,9 +325,12 @@ class C(Generic[P]):
def m(self, *args: P.args, **kwargs: P.kwargs) -> int:
return 1

c: C[Any]
reveal_type(c) # N: Revealed type is "__main__.C[Any]"
reveal_type(c.m) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> builtins.int"
c_old: C[Any]
reveal_type(c_old) # N: Revealed type is "__main__.C[[Any]]"
reveal_type(c_old.m) # N: Revealed type is "def (Any) -> builtins.int"
c: C[...]
reveal_type(c) # N: Revealed type is "__main__.C[...]"
reveal_type(c.m) # N: Revealed type is "def (*Any, **Any) -> builtins.int"
c.m(4, 6, y='x')
c = c

Expand Down Expand Up @@ -1520,3 +1523,16 @@ def identity(func: Callable[P, None]) -> Callable[P, None]: ...
@identity
def f(f: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ...
[builtins fixtures/paramspec.pyi]

[case testRuntimeSpecialParamspecLiteralSyntax]
import sub

reveal_type(sub.Ex[None]()) # N: Revealed type is "sub.Ex[[None]]"
[file sub/__init__.py]
from typing_extensions import ParamSpec
from typing import Generic

P = ParamSpec("P")

class Ex(Generic[P]): ...
[builtins fixtures/paramspec.pyi]