Skip to content

Commit

Permalink
[mypyc] Basic support for class-based named tuples (#10252)
Browse files Browse the repository at this point in the history
Not all possible variants are tested, but at least simple things work.

Fixes mypyc/mypyc#719.
  • Loading branch information
JukkaL authored Apr 23, 2021
1 parent 0603ae4 commit 8c82dac
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 1 deletion.
18 changes: 17 additions & 1 deletion mypyc/irbuild/classdef.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,16 @@ def populate_non_ext_bases(builder: IRBuilder, cdef: ClassDef) -> Value:
The tuple is passed to the metaclass constructor.
"""
is_named_tuple = cdef.info.is_named_tuple
ir = builder.mapper.type_to_ir[cdef.info]
bases = []
for cls in cdef.info.mro[1:]:
if cls.fullname == 'builtins.object':
continue
if is_named_tuple and cls.fullname in ('typing.Sequence', 'typing.Iterable'):
# HAX: Synthesized base classes added by mypy don't exist at runtime, so skip them.
# This could break if they were added explicitly, though...
continue
# Add the current class to the base classes list of concrete subclasses
if cls in builder.mapper.type_to_ir:
base_ir = builder.mapper.type_to_ir[cls]
Expand All @@ -237,6 +242,13 @@ def populate_non_ext_bases(builder: IRBuilder, cdef: ClassDef) -> Value:
# In Python 3.9 TypedDict is not a real type.
name = '_TypedDict'
base = builder.get_module_attr(module, name, cdef.line)
elif is_named_tuple and cls.fullname == 'builtins.tuple':
if builder.options.capi_version < (3, 9):
name = 'NamedTuple'
else:
# This was changed in Python 3.9.
name = '_NamedTuple'
base = builder.get_module_attr('typing', name, cdef.line)
else:
base = builder.load_global_str(cls.name, cdef.line)
bases.append(base)
Expand All @@ -255,6 +267,10 @@ def find_non_ext_metaclass(builder: IRBuilder, cdef: ClassDef, bases: Value) ->
# In Python 3.9, the metaclass for class-based TypedDict is typing._TypedDictMeta.
# We can't easily calculate it generically, so special case it.
return builder.get_module_attr('typing', '_TypedDictMeta', cdef.line)
elif cdef.info.is_named_tuple and builder.options.capi_version >= (3, 9):
# In Python 3.9, the metaclass for class-based NamedTuple is typing.NamedTupleMeta.
# We can't easily calculate it generically, so special case it.
return builder.get_module_attr('typing', 'NamedTupleMeta', cdef.line)

declared_metaclass = builder.add(LoadAddress(type_object_op.type,
type_object_op.src, cdef.line))
Expand Down Expand Up @@ -455,7 +471,7 @@ def cache_class_attrs(builder: IRBuilder,
attrs_to_cache: List[Tuple[Lvalue, RType]],
cdef: ClassDef) -> None:
"""Add class attributes to be cached to the global cache."""
typ = builder.load_native_type_object(cdef.fullname)
typ = builder.load_native_type_object(cdef.info.fullname)
for lval, rtype in attrs_to_cache:
assert isinstance(lval, NameExpr)
rval = builder.py_get_attr(typ, lval.name, cdef.line)
Expand Down
2 changes: 2 additions & 0 deletions mypyc/irbuild/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ def is_extension_class(cdef: ClassDef) -> bool:
return False
if cdef.info.typeddict_type:
return False
if cdef.info.is_named_tuple:
return False
if (cdef.info.metaclass_type and cdef.info.metaclass_type.type.fullname not in (
'abc.ABCMeta', 'typing.TypingMeta', 'typing.GenericMeta')):
return False
Expand Down
1 change: 1 addition & 0 deletions mypyc/test-data/fixtures/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ def zip(x: Iterable[T], y: Iterable[S]) -> Iterator[Tuple[T, S]]: ...
def zip(x: Iterable[T], y: Iterable[S], z: Iterable[V]) -> Iterator[Tuple[T, S, V]]: ...
def eval(e: str) -> Any: ...
def abs(x: float) -> float: ...
def exit() -> None: ...

# Dummy definitions.
class classmethod: pass
Expand Down
19 changes: 19 additions & 0 deletions mypyc/test-data/run-misc.test
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,25 @@ def test_non_total_typed_dict() -> None:
assert d3['c'] == 3
assert d4['d'] == 4

[case testClassBasedNamedTuple]
from typing import NamedTuple
import sys

# Class-based NamedTuple requires Python 3.6+
version = sys.version_info[:2]
if version[0] == 3 and version[1] < 6:
exit()

class NT(NamedTuple):
a: int

def test_named_tuple() -> None:
t = NT(a=1)
assert t.a == 1
assert type(t) is NT
assert isinstance(t, tuple)
assert not isinstance(tuple([1]), NT)

[case testUnion]
from typing import Union

Expand Down

0 comments on commit 8c82dac

Please sign in to comment.