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

[PEP 695] Partial support for new type parameter syntax in Python 3.12 #17233

Merged
merged 42 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
725a224
WIP parse type variable definitions in function
JukkaL Dec 28, 2023
3058b84
WIP make function type vars work kind of
JukkaL Dec 28, 2023
fc23bcf
WIP parse new-style type params in classes
JukkaL Dec 29, 2023
61e971d
WIP make class type vars work partially
JukkaL Dec 29, 2023
47cb82d
WIP variance test case
JukkaL Dec 29, 2023
e56dd96
WIP first attempt at inference of variance
JukkaL Mar 21, 2024
21c3eb6
WIP infer variance on demand
JukkaL Mar 21, 2024
7e15173
Fall back to covariance if variance not ready, and support attributes
JukkaL Mar 22, 2024
b76e7eb
Fix join
JukkaL Mar 22, 2024
650468b
Test meet when not ready
JukkaL Mar 22, 2024
639a85b
WIP minimal support for inheritance
JukkaL May 3, 2024
2b4dbe1
Add inheritance test cases
JukkaL May 3, 2024
5081cb9
Add test cases
JukkaL May 3, 2024
d42026e
Support protocols
JukkaL May 7, 2024
9745e5b
Add generic function test cases
JukkaL May 7, 2024
ddb4f58
Basic support for PEP 695 type aliases
JukkaL May 8, 2024
a1ea487
Partial support for upper bounds
JukkaL May 8, 2024
0e41f88
Ssupport forward refs in upper bounds
JukkaL May 8, 2024
16ffbfb
Fix for invalid upper bound in generic functions
JukkaL May 8, 2024
c6bbe70
Add test case
JukkaL May 8, 2024
59434fa
Add missing visit methods
JukkaL May 8, 2024
84e3f11
Generate error in mypyc if compiling a type alias statement
JukkaL May 8, 2024
586eb64
Fix type alias self check
JukkaL May 9, 2024
6bbc046
Fix variance not ready handling in typestate
JukkaL May 9, 2024
ea39e96
Fix missing return value in infer_class_variances
JukkaL May 9, 2024
ad8c062
Fix false positives about undefined names
JukkaL May 9, 2024
6138266
Basic support for tuple subclasses
JukkaL May 9, 2024
98bd612
Black
JukkaL May 9, 2024
a172027
Refactor AST
JukkaL May 9, 2024
c46550a
Support value restriction
JukkaL May 9, 2024
1a56647
Support ParamSpec
JukkaL May 10, 2024
80e8c79
Support TypeVarTuple
JukkaL May 10, 2024
4838121
Add some incremental mode tests
JukkaL May 10, 2024
d88a03b
Fix issue with forward references in type aliases
JukkaL May 10, 2024
d8291f3
Minor tweaks
JukkaL May 10, 2024
6c9f73c
Support *Ts and **P in type aliases
JukkaL May 15, 2024
2c7e147
Refactor to share code
JukkaL May 15, 2024
ef1b473
Address feedback
JukkaL May 15, 2024
4819d60
Fix forward reference in type alias bound
JukkaL May 15, 2024
196b2a3
Fix crash on undefined name in generic method
JukkaL May 15, 2024
369a4af
Fix variance inference when using real typeshed stubs
JukkaL May 16, 2024
dba87a8
Try to infer any variances that can be inferred
JukkaL May 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 8 additions & 3 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
from mypy.state import state
from mypy.subtypes import (
find_member,
infer_class_variances,
is_callable_compatible,
is_equivalent,
is_more_precise,
Expand Down Expand Up @@ -2374,7 +2375,7 @@ def visit_class_def(self, defn: ClassDef) -> None:
self.allow_abstract_call = old_allow_abstract_call
# TODO: Apply the sig to the actual TypeInfo so we can handle decorators
# that completely swap out the type. (e.g. Callable[[Type[A]], Type[B]])
if typ.defn.type_vars:
if typ.defn.type_vars and typ.defn.type_args is None:
for base_inst in typ.bases:
for base_tvar, base_decl_tvar in zip(
base_inst.args, base_inst.type.defn.type_vars
Expand All @@ -2396,6 +2397,7 @@ def visit_class_def(self, defn: ClassDef) -> None:
self.check_protocol_variance(defn)
if not defn.has_incompatible_baseclass and defn.info.is_enum:
self.check_enum(defn)
infer_class_variances(defn.info)

def check_final_deletable(self, typ: TypeInfo) -> None:
# These checks are only for mypyc. Only perform some checks that are easier
Expand Down Expand Up @@ -2566,6 +2568,9 @@ def check_protocol_variance(self, defn: ClassDef) -> None:
if they are actually covariant/contravariant, since this may break
transitivity of subtyping, see PEP 544.
"""
if defn.type_args is not None:
# Using new-style syntax (PEP 695), so variance will be inferred
return
info = defn.info
object_type = Instance(info.mro[-1], [])
tvars = info.defn.type_vars
Expand Down Expand Up @@ -3412,8 +3417,8 @@ def check_final(self, s: AssignmentStmt | OperatorAssignmentStmt | AssignmentExp
if (
lv.node.final_unset_in_class
and not lv.node.final_set_in_init
and not self.is_stub
and # It is OK to skip initializer in stub files.
and not self.is_stub # It is OK to skip initializer in stub files.
and
# Avoid extra error messages, if there is no type in Final[...],
# then we already reported the error about missing r.h.s.
isinstance(s, AssignmentStmt)
Expand Down
106 changes: 78 additions & 28 deletions mypy/fastparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
ARG_POS,
ARG_STAR,
ARG_STAR2,
PARAM_SPEC_KIND,
TYPE_VAR_KIND,
TYPE_VAR_TUPLE_KIND,
ArgKind,
Argument,
AssertStmt,
Expand Down Expand Up @@ -79,6 +82,8 @@
TempNode,
TryStmt,
TupleExpr,
TypeAliasStmt,
TypeParam,
UnaryExpr,
Var,
WhileStmt,
Expand All @@ -87,7 +92,7 @@
YieldFromExpr,
check_arg_names,
)
from mypy.options import Options
from mypy.options import NEW_GENERIC_SYNTAX, Options
from mypy.patterns import (
AsPattern,
ClassPattern,
Expand Down Expand Up @@ -144,11 +149,6 @@ def ast3_parse(
NamedExpr = ast3.NamedExpr
Constant = ast3.Constant

if sys.version_info >= (3, 12):
ast_TypeAlias = ast3.TypeAlias
else:
ast_TypeAlias = Any

if sys.version_info >= (3, 10):
Match = ast3.Match
MatchValue = ast3.MatchValue
Expand All @@ -171,11 +171,21 @@ def ast3_parse(
MatchAs = Any
MatchOr = Any
AstNode = Union[ast3.expr, ast3.stmt, ast3.ExceptHandler]

if sys.version_info >= (3, 11):
TryStar = ast3.TryStar
else:
TryStar = Any

if sys.version_info >= (3, 12):
ast_TypeAlias = ast3.TypeAlias
ast_ParamSpec = ast3.ParamSpec
ast_TypeVarTuple = ast3.TypeVarTuple
else:
ast_TypeAlias = Any
ast_ParamSpec = Any
ast_TypeVarTuple = Any

N = TypeVar("N", bound=Node)

# There is no way to create reasonable fallbacks at this stage,
Expand Down Expand Up @@ -884,6 +894,8 @@ def do_func_def(

arg_kinds = [arg.kind for arg in args]
arg_names = [None if arg.pos_only else arg.variable.name for arg in args]
# Type parameters, if using new syntax for generics (PEP 695)
explicit_type_params: list[TypeParam] | None = None

arg_types: list[Type | None] = []
if no_type_check:
Expand Down Expand Up @@ -937,12 +949,17 @@ def do_func_def(
return_type = AnyType(TypeOfAny.from_error)
else:
if sys.version_info >= (3, 12) and n.type_params:
self.fail(
ErrorMessage("PEP 695 generics are not yet supported", code=codes.VALID_TYPE),
n.type_params[0].lineno,
n.type_params[0].col_offset,
blocker=False,
)
if NEW_GENERIC_SYNTAX in self.options.enable_incomplete_feature:
explicit_type_params = self.translate_type_params(n.type_params)
else:
self.fail(
ErrorMessage(
"PEP 695 generics are not yet supported", code=codes.VALID_TYPE
),
n.type_params[0].lineno,
n.type_params[0].col_offset,
blocker=False,
)

arg_types = [a.type_annotation for a in args]
return_type = TypeConverter(
Expand Down Expand Up @@ -986,7 +1003,7 @@ def do_func_def(
self.class_and_function_stack.pop()
self.class_and_function_stack.append("F")
body = self.as_required_block(n.body, can_strip=True, is_coroutine=is_coroutine)
func_def = FuncDef(n.name, args, body, func_type)
func_def = FuncDef(n.name, args, body, func_type, explicit_type_params)
if isinstance(func_def.type, CallableType):
# semanal.py does some in-place modifications we want to avoid
func_def.unanalyzed_type = func_def.type.copy_modified()
Expand Down Expand Up @@ -1120,13 +1137,19 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef:
self.class_and_function_stack.append("C")
keywords = [(kw.arg, self.visit(kw.value)) for kw in n.keywords if kw.arg]

# Type parameters, if using new syntax for generics (PEP 695)
explicit_type_params: list[TypeParam] | None = None

if sys.version_info >= (3, 12) and n.type_params:
self.fail(
ErrorMessage("PEP 695 generics are not yet supported", code=codes.VALID_TYPE),
n.type_params[0].lineno,
n.type_params[0].col_offset,
blocker=False,
)
if NEW_GENERIC_SYNTAX in self.options.enable_incomplete_feature:
explicit_type_params = self.translate_type_params(n.type_params)
else:
self.fail(
ErrorMessage("PEP 695 generics are not yet supported", code=codes.VALID_TYPE),
n.type_params[0].lineno,
n.type_params[0].col_offset,
blocker=False,
)

cdef = ClassDef(
n.name,
Expand All @@ -1135,6 +1158,7 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef:
self.translate_expr_list(n.bases),
metaclass=dict(keywords).get("metaclass"),
keywords=keywords,
type_args=explicit_type_params,
)
cdef.decorators = self.translate_expr_list(n.decorator_list)
# Set lines to match the old mypy 0.700 lines, in order to keep
Expand All @@ -1150,6 +1174,24 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef:
self.class_and_function_stack.pop()
return cdef

def translate_type_params(self, type_params: list[Any]) -> list[TypeParam]:
explicit_type_params = []
for p in type_params:
bound = None
values: list[Type] = []
if isinstance(p, ast_ParamSpec): # type: ignore[misc]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can wait for another PR, but we should also check for the .default_value here for PEP 696 support.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably do that in a followup. Should be easy enough to implement support for PEP 696 here as well, although that requires Python 3.13 anyway.

explicit_type_params.append(TypeParam(p.name, PARAM_SPEC_KIND, None, []))
elif isinstance(p, ast_TypeVarTuple): # type: ignore[misc]
explicit_type_params.append(TypeParam(p.name, TYPE_VAR_TUPLE_KIND, None, []))
else:
if isinstance(p.bound, ast3.Tuple):
conv = TypeConverter(self.errors, line=p.lineno)
values = [conv.visit(t) for t in p.bound.elts]
elif p.bound is not None:
bound = TypeConverter(self.errors, line=p.lineno).visit(p.bound)
explicit_type_params.append(TypeParam(p.name, TYPE_VAR_KIND, bound, values))
return explicit_type_params

# Return(expr? value)
def visit_Return(self, n: ast3.Return) -> ReturnStmt:
node = ReturnStmt(self.visit(n.value))
Expand Down Expand Up @@ -1735,15 +1777,23 @@ def visit_MatchOr(self, n: MatchOr) -> OrPattern:
node = OrPattern([self.visit(pattern) for pattern in n.patterns])
return self.set_line(node, n)

def visit_TypeAlias(self, n: ast_TypeAlias) -> AssignmentStmt:
self.fail(
ErrorMessage("PEP 695 type aliases are not yet supported", code=codes.VALID_TYPE),
n.lineno,
n.col_offset,
blocker=False,
)
node = AssignmentStmt([NameExpr(n.name.id)], self.visit(n.value))
return self.set_line(node, n)
# TypeAlias(identifier name, type_param* type_params, expr value)
def visit_TypeAlias(self, n: ast_TypeAlias) -> TypeAliasStmt | AssignmentStmt:
node: TypeAliasStmt | AssignmentStmt
if NEW_GENERIC_SYNTAX in self.options.enable_incomplete_feature:
type_params = self.translate_type_params(n.type_params)
value = self.visit(n.value)
node = TypeAliasStmt(self.visit_Name(n.name), type_params, value)
return self.set_line(node, n)
else:
self.fail(
ErrorMessage("PEP 695 type aliases are not yet supported", code=codes.VALID_TYPE),
n.lineno,
n.col_offset,
blocker=False,
)
node = AssignmentStmt([NameExpr(n.name.id)], self.visit(n.value))
return self.set_line(node, n)


class TypeConverter:
Expand Down
4 changes: 2 additions & 2 deletions mypy/join.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import mypy.typeops
from mypy.maptype import map_instance_to_supertype
from mypy.nodes import CONTRAVARIANT, COVARIANT, INVARIANT
from mypy.nodes import CONTRAVARIANT, COVARIANT, INVARIANT, VARIANCE_NOT_READY
from mypy.state import state
from mypy.subtypes import (
SubtypeContext,
Expand Down Expand Up @@ -97,7 +97,7 @@ def join_instances(self, t: Instance, s: Instance) -> ProperType:
elif isinstance(sa_proper, AnyType):
new_type = AnyType(TypeOfAny.from_another_any, sa_proper)
elif isinstance(type_var, TypeVarType):
if type_var.variance == COVARIANT:
if type_var.variance in (COVARIANT, VARIANCE_NOT_READY):
new_type = join_types(ta, sa, self)
if len(type_var.values) != 0 and new_type not in type_var.values:
self.seen_instances.pop()
Expand Down
54 changes: 53 additions & 1 deletion mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,28 @@ def set_line(
self.variable.set_line(self.line, self.column, self.end_line, self.end_column)


# These specify the kind of a TypeParam
TYPE_VAR_KIND: Final = 0
PARAM_SPEC_KIND: Final = 1
TYPE_VAR_TUPLE_KIND: Final = 2


class TypeParam:
__slots__ = ("name", "kind", "upper_bound", "values")

def __init__(
self,
name: str,
kind: int,
upper_bound: mypy.types.Type | None,
values: list[mypy.types.Type],
) -> None:
self.name = name
self.kind = kind
self.upper_bound = upper_bound
self.values = values


FUNCITEM_FLAGS: Final = FUNCBASE_FLAGS + [
"is_overload",
"is_generator",
Expand All @@ -672,6 +694,7 @@ class FuncItem(FuncBase):
"min_args", # Minimum number of arguments
"max_pos", # Maximum number of positional arguments, -1 if no explicit
# limit (*args not included)
"type_args", # New-style type parameters (PEP 695)
"body", # Body of the function
"is_overload", # Is this an overload variant of function with more than
# one overload variant?
Expand All @@ -689,12 +712,14 @@ def __init__(
arguments: list[Argument] | None = None,
body: Block | None = None,
typ: mypy.types.FunctionLike | None = None,
type_args: list[TypeParam] | None = None,
) -> None:
super().__init__()
self.arguments = arguments or []
self.arg_names = [None if arg.pos_only else arg.variable.name for arg in self.arguments]
self.arg_kinds: list[ArgKind] = [arg.kind for arg in self.arguments]
self.max_pos: int = self.arg_kinds.count(ARG_POS) + self.arg_kinds.count(ARG_OPT)
self.type_args: list[TypeParam] | None = type_args
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new field should be serialized.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we actually need that here. Could be missing something but my understanding is that type_args is only used for the SemanticAnalyzer so there isn't a need to serialize it.

If it were necessary, mypy would've probably crashed for me while testing the PR with Home Assistant.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be right, I don't actually understand how our serialization works :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is just part of the syntax and doesn't get used beyond semantic analysis.

self.body: Block = body or Block([])
self.type = typ
self.unanalyzed_type = typ
Expand Down Expand Up @@ -761,8 +786,9 @@ def __init__(
arguments: list[Argument] | None = None,
body: Block | None = None,
typ: mypy.types.FunctionLike | None = None,
type_args: list[TypeParam] | None = None,
) -> None:
super().__init__(arguments, body, typ)
super().__init__(arguments, body, typ, type_args)
self._name = name
self.is_decorated = False
self.is_conditional = False # Defined conditionally (within block)?
Expand Down Expand Up @@ -1070,6 +1096,7 @@ class ClassDef(Statement):
"name",
"_fullname",
"defs",
"type_args",
"type_vars",
"base_type_exprs",
"removed_base_type_exprs",
Expand All @@ -1089,6 +1116,9 @@ class ClassDef(Statement):
name: str # Name of the class without module prefix
_fullname: str # Fully qualified name of the class
defs: Block
# New-style type parameters (PEP 695), unanalyzed
type_args: list[TypeParam] | None
# Semantically analyzed type parameters (all syntax variants)
type_vars: list[mypy.types.TypeVarLikeType]
# Base class expressions (not semantically analyzed -- can be arbitrary expressions)
base_type_exprs: list[Expression]
Expand All @@ -1111,12 +1141,14 @@ def __init__(
base_type_exprs: list[Expression] | None = None,
metaclass: Expression | None = None,
keywords: list[tuple[str, Expression]] | None = None,
type_args: list[TypeParam] | None = None,
) -> None:
super().__init__()
self.name = name
self._fullname = ""
self.defs = defs
self.type_vars = type_vars or []
self.type_args = type_args
self.base_type_exprs = base_type_exprs or []
self.removed_base_type_exprs = []
self.info = CLASSDEF_NO_INFO
Expand Down Expand Up @@ -1607,6 +1639,25 @@ def accept(self, visitor: StatementVisitor[T]) -> T:
return visitor.visit_match_stmt(self)


class TypeAliasStmt(Statement):
__slots__ = ("name", "type_args", "value")

__match_args__ = ("name", "type_args", "value")

name: NameExpr
type_args: list[TypeParam]
value: Expression # Will get translated into a type

def __init__(self, name: NameExpr, type_args: list[TypeParam], value: Expression) -> None:
super().__init__()
self.name = name
self.type_args = type_args
self.value = value

def accept(self, visitor: StatementVisitor[T]) -> T:
return visitor.visit_type_alias_stmt(self)


# Expressions


Expand Down Expand Up @@ -2442,6 +2493,7 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
INVARIANT: Final = 0
COVARIANT: Final = 1
CONTRAVARIANT: Final = 2
VARIANCE_NOT_READY: Final = 3 # Variance hasn't been inferred (using Python 3.12 syntax)


class TypeVarLikeExpr(SymbolNode, Expression):
Expand Down
3 changes: 2 additions & 1 deletion mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ class BuildType:
TYPE_VAR_TUPLE: Final = "TypeVarTuple"
UNPACK: Final = "Unpack"
PRECISE_TUPLE_TYPES: Final = "PreciseTupleTypes"
INCOMPLETE_FEATURES: Final = frozenset((PRECISE_TUPLE_TYPES,))
NEW_GENERIC_SYNTAX: Final = "NewGenericSyntax"
INCOMPLETE_FEATURES: Final = frozenset((PRECISE_TUPLE_TYPES, NEW_GENERIC_SYNTAX))
COMPLETE_FEATURES: Final = frozenset((TYPE_VAR_TUPLE, UNPACK))


Expand Down