Skip to content

Commit

Permalink
Basic support for typing_extensions.NamedTuple (#13178)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexWaygood committed Jul 19, 2022
1 parent 1a1091e commit fbbcc50
Show file tree
Hide file tree
Showing 7 changed files with 34 additions and 10 deletions.
2 changes: 1 addition & 1 deletion mypy/nodes.py
Expand Up @@ -2417,7 +2417,7 @@ class NamedTupleExpr(Expression):
# The class representation of this named tuple (its tuple_type attribute contains
# the tuple item types)
info: "TypeInfo"
is_typed: bool # whether this class was created with typing.NamedTuple
is_typed: bool # whether this class was created with typing(_extensions).NamedTuple

def __init__(self, info: 'TypeInfo', is_typed: bool = False) -> None:
super().__init__()
Expand Down
4 changes: 2 additions & 2 deletions mypy/semanal.py
Expand Up @@ -99,7 +99,7 @@
TypeTranslator, TypeOfAny, TypeType, NoneType, PlaceholderType, TPDICT_NAMES, ProperType,
get_proper_type, get_proper_types, TypeAliasType, TypeVarLikeType, Parameters, ParamSpecType,
PROTOCOL_NAMES, TYPE_ALIAS_NAMES, FINAL_TYPE_NAMES, FINAL_DECORATOR_NAMES, REVEAL_TYPE_NAMES,
ASSERT_TYPE_NAMES, OVERLOAD_NAMES, is_named_instance,
ASSERT_TYPE_NAMES, OVERLOAD_NAMES, TYPED_NAMEDTUPLE_NAMES, is_named_instance,
)
from mypy.typeops import function_type, get_type_vars
from mypy.type_visitor import TypeQuery
Expand Down Expand Up @@ -1529,7 +1529,7 @@ def analyze_base_classes(
bases = []
for base_expr in base_type_exprs:
if (isinstance(base_expr, RefExpr) and
base_expr.fullname in ('typing.NamedTuple',) + TPDICT_NAMES):
base_expr.fullname in TYPED_NAMEDTUPLE_NAMES + TPDICT_NAMES):
# Ignore magic bases for now.
continue

Expand Down
10 changes: 5 additions & 5 deletions mypy/semanal_namedtuple.py
Expand Up @@ -9,7 +9,7 @@

from mypy.types import (
Type, TupleType, AnyType, TypeOfAny, CallableType, TypeType, TypeVarType,
UnboundType, LiteralType,
UnboundType, LiteralType, TYPED_NAMEDTUPLE_NAMES
)
from mypy.semanal_shared import (
SemanticAnalyzerInterface, set_callable_name, calculate_tuple_fallback, PRIORITY_FALLBACKS
Expand Down Expand Up @@ -65,7 +65,7 @@ def analyze_namedtuple_classdef(self, defn: ClassDef, is_stub_file: bool,
for base_expr in defn.base_type_exprs:
if isinstance(base_expr, RefExpr):
self.api.accept(base_expr)
if base_expr.fullname == 'typing.NamedTuple':
if base_expr.fullname in TYPED_NAMEDTUPLE_NAMES:
result = self.check_namedtuple_classdef(defn, is_stub_file)
if result is None:
# This is a valid named tuple, but some types are incomplete.
Expand Down Expand Up @@ -175,7 +175,7 @@ def check_namedtuple(self,
fullname = callee.fullname
if fullname == 'collections.namedtuple':
is_typed = False
elif fullname == 'typing.NamedTuple':
elif fullname in TYPED_NAMEDTUPLE_NAMES:
is_typed = True
else:
return None, None
Expand Down Expand Up @@ -270,7 +270,7 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str
Return None if the definition didn't typecheck.
"""
type_name = 'NamedTuple' if fullname == 'typing.NamedTuple' else 'namedtuple'
type_name = 'NamedTuple' if fullname in TYPED_NAMEDTUPLE_NAMES else 'namedtuple'
# TODO: Share code with check_argument_count in checkexpr.py?
args = call.args
if len(args) < 2:
Expand All @@ -279,7 +279,7 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str
defaults: List[Expression] = []
if len(args) > 2:
# Typed namedtuple doesn't support additional arguments.
if fullname == 'typing.NamedTuple':
if fullname in TYPED_NAMEDTUPLE_NAMES:
self.fail('Too many arguments for "NamedTuple()"', call)
return None
for i, arg_name in enumerate(call.arg_names[2:], 2):
Expand Down
4 changes: 2 additions & 2 deletions mypy/subtypes.py
Expand Up @@ -8,7 +8,7 @@
Instance, TypeVarType, CallableType, TupleType, TypedDictType, UnionType, Overloaded,
ErasedType, PartialType, DeletedType, UninhabitedType, TypeType, is_named_instance,
FunctionLike, TypeOfAny, LiteralType, get_proper_type, TypeAliasType, ParamSpecType,
Parameters, UnpackType, TUPLE_LIKE_INSTANCE_NAMES, TypeVarTupleType,
Parameters, UnpackType, TUPLE_LIKE_INSTANCE_NAMES, TYPED_NAMEDTUPLE_NAMES, TypeVarTupleType,
)
import mypy.applytype
import mypy.constraints
Expand Down Expand Up @@ -286,7 +286,7 @@ def visit_instance(self, left: Instance) -> bool:
# in `TypeInfo.mro`, so when `(a: NamedTuple) -> None` is used,
# we need to check for `is_named_tuple` property
if ((left.type.has_base(rname) or rname == 'builtins.object'
or (rname == 'typing.NamedTuple'
or (rname in TYPED_NAMEDTUPLE_NAMES
and any(l.is_named_tuple for l in left.type.mro)))
and not self.ignore_declared_variance):
# Map left type to corresponding right instances.
Expand Down
5 changes: 5 additions & 0 deletions mypy/types.py
Expand Up @@ -66,6 +66,11 @@
SyntheticTypeVisitor as SyntheticTypeVisitor,
)

TYPED_NAMEDTUPLE_NAMES: Final = (
"typing.NamedTuple",
"typing_extensions.NamedTuple"
)

# Supported names of TypedDict type constructors.
TPDICT_NAMES: Final = (
"typing.TypedDict",
Expand Down
18 changes: 18 additions & 0 deletions test-data/unit/check-class-namedtuple.test
Expand Up @@ -709,3 +709,21 @@ class HasStaticMethod(NamedTuple):
return 4

[builtins fixtures/property.pyi]

[case testTypingExtensionsNamedTuple]
from typing_extensions import NamedTuple

class Point(NamedTuple):
x: int
y: int

bad_point = Point('foo') # E: Missing positional argument "y" in call to "Point" \
# E: Argument 1 to "Point" has incompatible type "str"; expected "int"
point = Point(1, 2)
x, y = point
x = point.x
reveal_type(x) # N: Revealed type is "builtins.int"
reveal_type(y) # N: Revealed type is "builtins.int"
point.y = 6 # E: Property "y" defined in "Point" is read-only

[builtins fixtures/tuple.pyi]
1 change: 1 addition & 0 deletions test-data/unit/lib-stub/typing_extensions.pyi
Expand Up @@ -10,6 +10,7 @@ class _SpecialForm:
def __getitem__(self, typeargs: Any) -> Any:
pass

NamedTuple = 0
Protocol: _SpecialForm = ...
def runtime_checkable(x: _T) -> _T: pass
runtime = runtime_checkable
Expand Down

0 comments on commit fbbcc50

Please sign in to comment.