Skip to content

Commit

Permalink
Add TypeVarTupleExpr node (#12481)
Browse files Browse the repository at this point in the history
This adds minimal support for a TypeVarTupleExpr node, gated behind the
flag to enable incomplete features. It is modeled after paramspec,
including the part where we don't support the various arguments that
have no behavior defined in PEP646.

We also include TypeVarTuple in the typing_extensions stubs for test
data and add some very basic semanal tests to verify the basic things work.
  • Loading branch information
jhance committed Apr 20, 2022
1 parent 8b1a810 commit 50213b5
Show file tree
Hide file tree
Showing 12 changed files with 134 additions and 7 deletions.
5 changes: 4 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
DictionaryComprehension, ComplexExpr, EllipsisExpr, StarExpr, AwaitExpr, YieldExpr,
YieldFromExpr, TypedDictExpr, PromoteExpr, NewTypeExpr, NamedTupleExpr, TypeVarExpr,
TypeAliasExpr, BackquoteExpr, EnumCallExpr, TypeAlias, SymbolNode, PlaceholderNode,
ParamSpecExpr,
ParamSpecExpr, TypeVarTupleExpr,
ArgKind, ARG_POS, ARG_NAMED, ARG_STAR, ARG_STAR2, LITERAL_TYPE, REVEAL_TYPE,
)
from mypy.literals import literal
Expand Down Expand Up @@ -4186,6 +4186,9 @@ def visit_type_var_expr(self, e: TypeVarExpr) -> Type:
def visit_paramspec_expr(self, e: ParamSpecExpr) -> Type:
return AnyType(TypeOfAny.special_form)

def visit_type_var_tuple_expr(self, e: TypeVarTupleExpr) -> Type:
return AnyType(TypeOfAny.special_form)

def visit_newtype_expr(self, e: NewTypeExpr) -> Type:
return AnyType(TypeOfAny.special_form)

Expand Down
5 changes: 4 additions & 1 deletion mypy/literals.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
TypeApplication, LambdaExpr, ListComprehension, SetComprehension, DictionaryComprehension,
GeneratorExpr, BackquoteExpr, TypeVarExpr, TypeAliasExpr, NamedTupleExpr, EnumCallExpr,
TypedDictExpr, NewTypeExpr, PromoteExpr, AwaitExpr, TempNode, AssignmentExpr, ParamSpecExpr,
AssertTypeExpr,
AssertTypeExpr, TypeVarTupleExpr,
)
from mypy.visitor import ExpressionVisitor

Expand Down Expand Up @@ -224,6 +224,9 @@ def visit_type_var_expr(self, e: TypeVarExpr) -> None:
def visit_paramspec_expr(self, e: ParamSpecExpr) -> None:
return None

def visit_type_var_tuple_expr(self, e: TypeVarTupleExpr) -> None:
return None

def visit_type_alias_expr(self, e: TypeAliasExpr) -> None:
return None

Expand Down
33 changes: 32 additions & 1 deletion mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2233,7 +2233,10 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:


class TypeVarLikeExpr(SymbolNode, Expression):
"""Base class for TypeVarExpr and ParamSpecExpr."""
"""Base class for TypeVarExpr, ParamSpecExpr and TypeVarTupleExpr.
Note that they are constructed by the semantic analyzer.
"""

__slots__ = ('_name', '_fullname', 'upper_bound', 'variance')

Expand Down Expand Up @@ -2339,6 +2342,34 @@ def deserialize(cls, data: JsonDict) -> 'ParamSpecExpr':
)


class TypeVarTupleExpr(TypeVarLikeExpr):
"""Type variable tuple expression TypeVarTuple(...)."""

__slots__ = ()

def accept(self, visitor: ExpressionVisitor[T]) -> T:
return visitor.visit_type_var_tuple_expr(self)

def serialize(self) -> JsonDict:
return {
'.class': 'TypeVarTupleExpr',
'name': self._name,
'fullname': self._fullname,
'upper_bound': self.upper_bound.serialize(),
'variance': self.variance,
}

@classmethod
def deserialize(cls, data: JsonDict) -> 'TypeVarTupleExpr':
assert data['.class'] == 'TypeVarTupleExpr'
return TypeVarTupleExpr(
data['name'],
data['fullname'],
mypy.types.deserialize_type(data['upper_bound']),
data['variance']
)


class TypeAliasExpr(Expression):
"""Type alias expression (rvalue)."""

Expand Down
41 changes: 40 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
typing_extensions_aliases,
EnumCallExpr, RUNTIME_PROTOCOL_DECOS, FakeExpression, Statement, AssignmentExpr,
ParamSpecExpr, EllipsisExpr, TypeVarLikeExpr, implicit_module_attrs,
MatchStmt, FuncBase
MatchStmt, FuncBase, TypeVarTupleExpr
)
from mypy.patterns import (
AsPattern, OrPattern, ValuePattern, SequencePattern,
Expand Down Expand Up @@ -2074,6 +2074,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
special_form = True
elif self.process_paramspec_declaration(s):
special_form = True
elif self.process_typevartuple_declaration(s):
special_form = True
# * type constructors
elif self.analyze_namedtuple_assign(s):
special_form = True
Expand Down Expand Up @@ -3332,6 +3334,43 @@ def process_paramspec_declaration(self, s: AssignmentStmt) -> bool:
self.add_symbol(name, call.analyzed, s)
return True

def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool:
"""Checks if s declares a TypeVarTuple; if yes, store it in symbol table.
Return True if this looks like a TypeVarTuple (maybe with errors), otherwise return False.
"""
call = self.get_typevarlike_declaration(
s, ("typing_extensions.TypeVarTuple", "typing.TypeVarTuple")
)
if not call:
return False

if len(call.args) > 1:
self.fail(
"Only the first argument to TypeVarTuple has defined semantics",
s,
)

if not self.options.enable_incomplete_features:
self.fail('"TypeVarTuple" is not supported by mypy yet', s)
return False

name = self.extract_typevarlike_name(s, call)
if name is None:
return False

# PEP 646 does not specify the behavior of variance, constraints, or bounds.
if not call.analyzed:
typevartuple_var = TypeVarTupleExpr(
name, self.qualified_name(name), self.object_type(), INVARIANT
)
typevartuple_var.line = call.line
call.analyzed = typevartuple_var
else:
assert isinstance(call.analyzed, TypeVarTupleExpr)
self.add_symbol(name, call.analyzed, s)
return True

def basic_new_typeinfo(self, name: str,
basetype_or_fallback: Instance,
line: int) -> TypeInfo:
Expand Down
12 changes: 12 additions & 0 deletions mypy/strconv.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,18 @@ def visit_paramspec_expr(self, o: 'mypy.nodes.ParamSpecExpr') -> str:
a += ['UpperBound({})'.format(o.upper_bound)]
return self.dump(a, o)

def visit_type_var_tuple_expr(self, o: 'mypy.nodes.TypeVarTupleExpr') -> str:
import mypy.types

a: List[Any] = []
if o.variance == mypy.nodes.COVARIANT:
a += ['Variance(COVARIANT)']
if o.variance == mypy.nodes.CONTRAVARIANT:
a += ['Variance(CONTRAVARIANT)']
if not mypy.types.is_named_instance(o.upper_bound, 'builtins.object'):
a += ['UpperBound({})'.format(o.upper_bound)]
return self.dump(a, o)

def visit_type_alias_expr(self, o: 'mypy.nodes.TypeAliasExpr') -> str:
return 'TypeAliasExpr({})'.format(o.type)

Expand Down
5 changes: 4 additions & 1 deletion mypy/test/testtransform.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def test_transform(testcase: DataDrivenTestCase) -> None:
options = parse_options(src, testcase, 1)
options.use_builtins_fixtures = True
options.semantic_analysis_only = True
options.enable_incomplete_features = True
options.show_traceback = True
result = build.build(sources=[BuildSource('main', None, src)],
options=options,
Expand All @@ -54,8 +55,10 @@ def test_transform(testcase: DataDrivenTestCase) -> None:
# path.
# TODO the test is not reliable
if (not f.path.endswith((os.sep + 'builtins.pyi',
'typing_extensions.pyi',
'typing.pyi',
'abc.pyi'))
'abc.pyi',
'sys.pyi'))
and not os.path.basename(f.path).startswith('_')
and not os.path.splitext(
os.path.basename(f.path))[0].endswith('_')):
Expand Down
7 changes: 6 additions & 1 deletion mypy/treetransform.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
YieldFromExpr, NamedTupleExpr, TypedDictExpr, NonlocalDecl, SetComprehension,
DictionaryComprehension, ComplexExpr, TypeAliasExpr, EllipsisExpr,
YieldExpr, ExecStmt, Argument, BackquoteExpr, AwaitExpr, AssignmentExpr,
OverloadPart, EnumCallExpr, REVEAL_TYPE, GDEF
OverloadPart, EnumCallExpr, REVEAL_TYPE, GDEF, TypeVarTupleExpr
)
from mypy.types import Type, FunctionLike, ProperType
from mypy.traverser import TraverserVisitor
Expand Down Expand Up @@ -515,6 +515,11 @@ def visit_paramspec_expr(self, node: ParamSpecExpr) -> ParamSpecExpr:
node.name, node.fullname, self.type(node.upper_bound), variance=node.variance
)

def visit_type_var_tuple_expr(self, node: TypeVarTupleExpr) -> TypeVarTupleExpr:
return TypeVarTupleExpr(
node.name, node.fullname, self.type(node.upper_bound), variance=node.variance
)

def visit_type_alias_expr(self, node: TypeAliasExpr) -> TypeAliasExpr:
return TypeAliasExpr(node.node)

Expand Down
7 changes: 7 additions & 0 deletions mypy/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ def visit_type_var_expr(self, o: 'mypy.nodes.TypeVarExpr') -> T:
def visit_paramspec_expr(self, o: 'mypy.nodes.ParamSpecExpr') -> T:
pass

@abstractmethod
def visit_type_var_tuple_expr(self, o: 'mypy.nodes.TypeVarTupleExpr') -> T:
pass

@abstractmethod
def visit_type_alias_expr(self, o: 'mypy.nodes.TypeAliasExpr') -> T:
pass
Expand Down Expand Up @@ -590,6 +594,9 @@ def visit_type_var_expr(self, o: 'mypy.nodes.TypeVarExpr') -> T:
def visit_paramspec_expr(self, o: 'mypy.nodes.ParamSpecExpr') -> T:
pass

def visit_type_var_tuple_expr(self, o: 'mypy.nodes.TypeVarTupleExpr') -> T:
pass

def visit_type_alias_expr(self, o: 'mypy.nodes.TypeAliasExpr') -> T:
pass

Expand Down
5 changes: 4 additions & 1 deletion mypyc/irbuild/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
NamedTupleExpr, NewTypeExpr, NonlocalDecl, OverloadedFuncDef, PrintStmt, RaiseStmt,
RevealExpr, SetExpr, SliceExpr, StarExpr, SuperExpr, TryStmt, TypeAliasExpr, TypeApplication,
TypeVarExpr, TypedDictExpr, UnicodeExpr, WithStmt, YieldFromExpr, YieldExpr, ParamSpecExpr,
MatchStmt
MatchStmt, TypeVarTupleExpr
)

from mypyc.ir.ops import Value
Expand Down Expand Up @@ -315,6 +315,9 @@ def visit_type_var_expr(self, o: TypeVarExpr) -> Value:
def visit_paramspec_expr(self, o: ParamSpecExpr) -> Value:
assert False, "can't compile analysis-only expressions"

def visit_type_var_tuple_expr(self, o: TypeVarTupleExpr) -> Value:
assert False, "can't compile analysis-only expressions"

def visit_typeddict_expr(self, o: TypedDictExpr) -> Value:
assert False, "can't compile analysis-only expressions"

Expand Down
1 change: 1 addition & 0 deletions test-data/unit/lib-stub/typing_extensions.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ TypeAlias: _SpecialForm
TypeGuard: _SpecialForm
Never: _SpecialForm

TypeVarTuple: _SpecialForm
Unpack: _SpecialForm

# Fallback type for all typed dicts (does not exist at runtime).
Expand Down
10 changes: 10 additions & 0 deletions test-data/unit/semanal-errors.test
Original file line number Diff line number Diff line change
Expand Up @@ -1460,3 +1460,13 @@ heterogenous_tuple: Tuple[Unpack[Tuple[int, str]]]
homogenous_tuple: Tuple[Unpack[Tuple[int, ...]]]
bad: Tuple[Unpack[int]] # E: builtins.int cannot be unpacked (must be tuple or TypeVarTuple)
[builtins fixtures/tuple.pyi]

[case testTypeVarTuple]
from typing_extensions import TypeVarTuple

TVariadic = TypeVarTuple('TVariadic')
TP = TypeVarTuple('?') # E: String argument 1 "?" to TypeVarTuple(...) does not match variable name "TP"
TP2: int = TypeVarTuple('TP2') # E: Cannot declare the type of a TypeVar or similar construct
TP3 = TypeVarTuple() # E: Too few arguments for TypeVarTuple()
TP4 = TypeVarTuple('TP4', 'TP4') # E: Only the first argument to TypeVarTuple has defined semantics
TP5 = TypeVarTuple(t='TP5') # E: TypeVarTuple() expects a string literal as first argument
10 changes: 10 additions & 0 deletions test-data/unit/semanal-types.test
Original file line number Diff line number Diff line change
Expand Up @@ -1548,3 +1548,13 @@ MypyFile:1(
AssignmentStmt:2(
NameExpr(P* [__main__.P])
ParamSpecExpr:2()))

[case testTypeVarTuple]
from typing_extensions import TypeVarTuple
TV = TypeVarTuple("TV")
[out]
MypyFile:1(
ImportFrom:1(typing_extensions, [TypeVarTuple])
AssignmentStmt:2(
NameExpr(TV* [__main__.TV])
TypeVarTupleExpr:2()))

0 comments on commit 50213b5

Please sign in to comment.