bpo-40185: Refactor typing.NamedTuple (GH-19371)

This commit is contained in:
Serhiy Storchaka 2020-04-08 10:59:04 +03:00 committed by GitHub
parent 307b9d0144
commit a2ec06938f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 53 additions and 58 deletions

View File

@ -3598,11 +3598,9 @@ class NamedTupleTests(BaseTestCase):
self.assertEqual(CoolEmployeeWithDefault._field_defaults, dict(cool=0)) self.assertEqual(CoolEmployeeWithDefault._field_defaults, dict(cool=0))
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
exec(""" class NonDefaultAfterDefault(NamedTuple):
class NonDefaultAfterDefault(NamedTuple): x: int = 3
x: int = 3 y: int
y: int
""")
def test_annotation_usage_with_methods(self): def test_annotation_usage_with_methods(self):
self.assertEqual(XMeth(1).double(), 2) self.assertEqual(XMeth(1).double(), 2)
@ -3611,20 +3609,16 @@ class NonDefaultAfterDefault(NamedTuple):
self.assertEqual(XRepr(1, 2) + XRepr(3), 0) self.assertEqual(XRepr(1, 2) + XRepr(3), 0)
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
exec(""" class XMethBad(NamedTuple):
class XMethBad(NamedTuple): x: int
x: int def _fields(self):
def _fields(self): return 'no chance for this'
return 'no chance for this'
""")
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
exec(""" class XMethBad2(NamedTuple):
class XMethBad2(NamedTuple): x: int
x: int def _source(self):
def _source(self): return 'no chance for this as well'
return 'no chance for this as well'
""")
def test_multiple_inheritance(self): def test_multiple_inheritance(self):
class A: class A:

View File

@ -1702,51 +1702,41 @@ class SupportsRound(Protocol[T_co]):
pass pass
def _make_nmtuple(name, types): def _make_nmtuple(name, types, module, defaults = ()):
msg = "NamedTuple('Name', [(f0, t0), (f1, t1), ...]); each t must be a type" fields = [n for n, t in types]
types = [(n, _type_check(t, msg)) for n, t in types] types = {n: _type_check(t, f"field {n} annotation must be a type")
nm_tpl = collections.namedtuple(name, [n for n, t in types]) for n, t in types}
nm_tpl.__annotations__ = dict(types) nm_tpl = collections.namedtuple(name, fields,
try: defaults=defaults, module=module)
nm_tpl.__module__ = sys._getframe(2).f_globals.get('__name__', '__main__') nm_tpl.__annotations__ = nm_tpl.__new__.__annotations__ = types
except (AttributeError, ValueError):
pass
return nm_tpl return nm_tpl
# attributes prohibited to set in NamedTuple class syntax # attributes prohibited to set in NamedTuple class syntax
_prohibited = {'__new__', '__init__', '__slots__', '__getnewargs__', _prohibited = frozenset({'__new__', '__init__', '__slots__', '__getnewargs__',
'_fields', '_field_defaults', '_fields', '_field_defaults',
'_make', '_replace', '_asdict', '_source'} '_make', '_replace', '_asdict', '_source'})
_special = {'__module__', '__name__', '__annotations__'} _special = frozenset({'__module__', '__name__', '__annotations__'})
class NamedTupleMeta(type): class NamedTupleMeta(type):
def __new__(cls, typename, bases, ns): def __new__(cls, typename, bases, ns):
if ns.get('_root', False): assert bases[0] is _NamedTuple
return super().__new__(cls, typename, bases, ns)
if len(bases) > 1:
raise TypeError("Multiple inheritance with NamedTuple is not supported")
assert bases[0] is NamedTuple
types = ns.get('__annotations__', {}) types = ns.get('__annotations__', {})
nm_tpl = _make_nmtuple(typename, types.items()) default_names = []
defaults = []
defaults_dict = {}
for field_name in types: for field_name in types:
if field_name in ns: if field_name in ns:
default_value = ns[field_name] default_names.append(field_name)
defaults.append(default_value) elif default_names:
defaults_dict[field_name] = default_value raise TypeError(f"Non-default namedtuple field {field_name} "
elif defaults: f"cannot follow default field"
raise TypeError("Non-default namedtuple field {field_name} cannot " f"{'s' if len(default_names) > 1 else ''} "
"follow default field(s) {default_names}" f"{', '.join(default_names)}")
.format(field_name=field_name, nm_tpl = _make_nmtuple(typename, types.items(),
default_names=', '.join(defaults_dict.keys()))) defaults=[ns[n] for n in default_names],
nm_tpl.__new__.__annotations__ = dict(types) module=ns['__module__'])
nm_tpl.__new__.__defaults__ = tuple(defaults)
nm_tpl._field_defaults = defaults_dict
# update from user namespace without overriding special namedtuple attributes # update from user namespace without overriding special namedtuple attributes
for key in ns: for key in ns:
if key in _prohibited: if key in _prohibited:
@ -1756,7 +1746,7 @@ class NamedTupleMeta(type):
return nm_tpl return nm_tpl
class NamedTuple(metaclass=NamedTupleMeta): def NamedTuple(typename, fields=None, /, **kwargs):
"""Typed version of namedtuple. """Typed version of namedtuple.
Usage in Python versions >= 3.6:: Usage in Python versions >= 3.6::
@ -1780,15 +1770,26 @@ class NamedTuple(metaclass=NamedTupleMeta):
Employee = NamedTuple('Employee', [('name', str), ('id', int)]) Employee = NamedTuple('Employee', [('name', str), ('id', int)])
""" """
_root = True if fields is None:
fields = kwargs.items()
elif kwargs:
raise TypeError("Either list of fields or keywords"
" can be provided to NamedTuple, not both")
try:
module = sys._getframe(1).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
module = None
return _make_nmtuple(typename, fields, module=module)
def __new__(cls, typename, fields=None, /, **kwargs): _NamedTuple = type.__new__(NamedTupleMeta, 'NamedTuple', (), {})
if fields is None:
fields = kwargs.items() def _namedtuple_mro_entries(bases):
elif kwargs: if len(bases) > 1:
raise TypeError("Either list of fields or keywords" raise TypeError("Multiple inheritance with NamedTuple is not supported")
" can be provided to NamedTuple, not both") assert bases[0] is NamedTuple
return _make_nmtuple(typename, fields) return (_NamedTuple,)
NamedTuple.__mro_entries__ = _namedtuple_mro_entries
def _dict_new(cls, /, *args, **kwargs): def _dict_new(cls, /, *args, **kwargs):