mirror of https://github.com/python/cpython.git
bpo-38191: Accept arbitrary keyword names in NamedTuple() and TypedDict(). (GH-16222)
This includes such names as "cls", "self", "typename", "_typename", "fields" and "_fields". Passing positional arguments by keyword is deprecated.
This commit is contained in:
parent
b57481318e
commit
2bf31ccab3
|
@ -3563,6 +3563,36 @@ def test_namedtuple_keyword_usage(self):
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
NamedTuple('Name', x=1, y='a')
|
NamedTuple('Name', x=1, y='a')
|
||||||
|
|
||||||
|
def test_namedtuple_special_keyword_names(self):
|
||||||
|
NT = NamedTuple("NT", cls=type, self=object, typename=str, fields=list)
|
||||||
|
self.assertEqual(NT.__name__, 'NT')
|
||||||
|
self.assertEqual(NT._fields, ('cls', 'self', 'typename', 'fields'))
|
||||||
|
a = NT(cls=str, self=42, typename='foo', fields=[('bar', tuple)])
|
||||||
|
self.assertEqual(a.cls, str)
|
||||||
|
self.assertEqual(a.self, 42)
|
||||||
|
self.assertEqual(a.typename, 'foo')
|
||||||
|
self.assertEqual(a.fields, [('bar', tuple)])
|
||||||
|
|
||||||
|
def test_namedtuple_errors(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
NamedTuple.__new__()
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
NamedTuple()
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
NamedTuple('Emp', [('name', str)], None)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
NamedTuple('Emp', [('_name', str)])
|
||||||
|
|
||||||
|
with self.assertWarns(DeprecationWarning):
|
||||||
|
Emp = NamedTuple(typename='Emp', name=str, id=int)
|
||||||
|
self.assertEqual(Emp.__name__, 'Emp')
|
||||||
|
self.assertEqual(Emp._fields, ('name', 'id'))
|
||||||
|
|
||||||
|
with self.assertWarns(DeprecationWarning):
|
||||||
|
Emp = NamedTuple('Emp', fields=[('name', str), ('id', int)])
|
||||||
|
self.assertEqual(Emp.__name__, 'Emp')
|
||||||
|
self.assertEqual(Emp._fields, ('name', 'id'))
|
||||||
|
|
||||||
def test_pickle(self):
|
def test_pickle(self):
|
||||||
global Emp # pickle wants to reference the class by name
|
global Emp # pickle wants to reference the class by name
|
||||||
Emp = NamedTuple('Emp', [('name', str), ('id', int)])
|
Emp = NamedTuple('Emp', [('name', str), ('id', int)])
|
||||||
|
@ -3604,6 +3634,36 @@ def test_basics_keywords_syntax(self):
|
||||||
self.assertEqual(Emp.__annotations__, {'name': str, 'id': int})
|
self.assertEqual(Emp.__annotations__, {'name': str, 'id': int})
|
||||||
self.assertEqual(Emp.__total__, True)
|
self.assertEqual(Emp.__total__, True)
|
||||||
|
|
||||||
|
def test_typeddict_special_keyword_names(self):
|
||||||
|
TD = TypedDict("TD", cls=type, self=object, typename=str, _typename=int, fields=list, _fields=dict)
|
||||||
|
self.assertEqual(TD.__name__, 'TD')
|
||||||
|
self.assertEqual(TD.__annotations__, {'cls': type, 'self': object, 'typename': str, '_typename': int, 'fields': list, '_fields': dict})
|
||||||
|
a = TD(cls=str, self=42, typename='foo', _typename=53, fields=[('bar', tuple)], _fields={'baz', set})
|
||||||
|
self.assertEqual(a['cls'], str)
|
||||||
|
self.assertEqual(a['self'], 42)
|
||||||
|
self.assertEqual(a['typename'], 'foo')
|
||||||
|
self.assertEqual(a['_typename'], 53)
|
||||||
|
self.assertEqual(a['fields'], [('bar', tuple)])
|
||||||
|
self.assertEqual(a['_fields'], {'baz', set})
|
||||||
|
|
||||||
|
def test_typeddict_create_errors(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
TypedDict.__new__()
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
TypedDict()
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
TypedDict('Emp', [('name', str)], None)
|
||||||
|
|
||||||
|
with self.assertWarns(DeprecationWarning):
|
||||||
|
Emp = TypedDict(_typename='Emp', name=str, id=int)
|
||||||
|
self.assertEqual(Emp.__name__, 'Emp')
|
||||||
|
self.assertEqual(Emp.__annotations__, {'name': str, 'id': int})
|
||||||
|
|
||||||
|
with self.assertWarns(DeprecationWarning):
|
||||||
|
Emp = TypedDict('Emp', _fields={'name': str, 'id': int})
|
||||||
|
self.assertEqual(Emp.__name__, 'Emp')
|
||||||
|
self.assertEqual(Emp.__annotations__, {'name': str, 'id': int})
|
||||||
|
|
||||||
def test_typeddict_errors(self):
|
def test_typeddict_errors(self):
|
||||||
Emp = TypedDict('Emp', {'name': str, 'id': int})
|
Emp = TypedDict('Emp', {'name': str, 'id': int})
|
||||||
self.assertEqual(TypedDict.__module__, 'typing')
|
self.assertEqual(TypedDict.__module__, 'typing')
|
||||||
|
|
|
@ -1653,35 +1653,96 @@ class Employee(NamedTuple):
|
||||||
"""
|
"""
|
||||||
_root = True
|
_root = True
|
||||||
|
|
||||||
def __new__(self, typename, fields=None, **kwargs):
|
def __new__(*args, **kwargs):
|
||||||
|
if not args:
|
||||||
|
raise TypeError('NamedTuple.__new__(): not enough arguments')
|
||||||
|
cls, *args = args # allow the "cls" keyword be passed
|
||||||
|
if args:
|
||||||
|
typename, *args = args # allow the "typename" keyword be passed
|
||||||
|
elif 'typename' in kwargs:
|
||||||
|
typename = kwargs.pop('typename')
|
||||||
|
import warnings
|
||||||
|
warnings.warn("Passing 'typename' as keyword argument is deprecated",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
else:
|
||||||
|
raise TypeError("NamedTuple.__new__() missing 1 required positional "
|
||||||
|
"argument: 'typename'")
|
||||||
|
if args:
|
||||||
|
try:
|
||||||
|
fields, = args # allow the "fields" keyword be passed
|
||||||
|
except ValueError:
|
||||||
|
raise TypeError(f'NamedTuple.__new__() takes from 2 to 3 '
|
||||||
|
f'positional arguments but {len(args) + 2} '
|
||||||
|
f'were given') from None
|
||||||
|
elif 'fields' in kwargs and len(kwargs) == 1:
|
||||||
|
fields = kwargs.pop('fields')
|
||||||
|
import warnings
|
||||||
|
warnings.warn("Passing 'fields' as keyword argument is deprecated",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
else:
|
||||||
|
fields = None
|
||||||
|
|
||||||
if fields is None:
|
if fields is None:
|
||||||
fields = kwargs.items()
|
fields = kwargs.items()
|
||||||
elif kwargs:
|
elif kwargs:
|
||||||
raise TypeError("Either list of fields or keywords"
|
raise TypeError("Either list of fields or keywords"
|
||||||
" can be provided to NamedTuple, not both")
|
" can be provided to NamedTuple, not both")
|
||||||
return _make_nmtuple(typename, fields)
|
return _make_nmtuple(typename, fields)
|
||||||
|
__new__.__text_signature__ = '($cls, typename, fields=None, /, **kwargs)'
|
||||||
|
|
||||||
|
|
||||||
def _dict_new(cls, *args, **kwargs):
|
def _dict_new(*args, **kwargs):
|
||||||
|
if not args:
|
||||||
|
raise TypeError('TypedDict.__new__(): not enough arguments')
|
||||||
|
cls, *args = args # allow the "cls" keyword be passed
|
||||||
return dict(*args, **kwargs)
|
return dict(*args, **kwargs)
|
||||||
|
_dict_new.__text_signature__ = '($cls, _typename, _fields=None, /, **kwargs)'
|
||||||
|
|
||||||
|
|
||||||
def _typeddict_new(cls, _typename, _fields=None, **kwargs):
|
def _typeddict_new(*args, total=True, **kwargs):
|
||||||
total = kwargs.pop('total', True)
|
if not args:
|
||||||
if _fields is None:
|
raise TypeError('TypedDict.__new__(): not enough arguments')
|
||||||
_fields = kwargs
|
cls, *args = args # allow the "cls" keyword be passed
|
||||||
|
if args:
|
||||||
|
typename, *args = args # allow the "_typename" keyword be passed
|
||||||
|
elif '_typename' in kwargs:
|
||||||
|
typename = kwargs.pop('_typename')
|
||||||
|
import warnings
|
||||||
|
warnings.warn("Passing '_typename' as keyword argument is deprecated",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
else:
|
||||||
|
raise TypeError("TypedDict.__new__() missing 1 required positional "
|
||||||
|
"argument: '_typename'")
|
||||||
|
if args:
|
||||||
|
try:
|
||||||
|
fields, = args # allow the "_fields" keyword be passed
|
||||||
|
except ValueError:
|
||||||
|
raise TypeError(f'TypedDict.__new__() takes from 2 to 3 '
|
||||||
|
f'positional arguments but {len(args) + 2} '
|
||||||
|
f'were given') from None
|
||||||
|
elif '_fields' in kwargs and len(kwargs) == 1:
|
||||||
|
fields = kwargs.pop('_fields')
|
||||||
|
import warnings
|
||||||
|
warnings.warn("Passing '_fields' as keyword argument is deprecated",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
else:
|
||||||
|
fields = None
|
||||||
|
|
||||||
|
if fields is None:
|
||||||
|
fields = kwargs
|
||||||
elif kwargs:
|
elif kwargs:
|
||||||
raise TypeError("TypedDict takes either a dict or keyword arguments,"
|
raise TypeError("TypedDict takes either a dict or keyword arguments,"
|
||||||
" but not both")
|
" but not both")
|
||||||
|
|
||||||
ns = {'__annotations__': dict(_fields), '__total__': total}
|
ns = {'__annotations__': dict(fields), '__total__': total}
|
||||||
try:
|
try:
|
||||||
# Setting correct module is necessary to make typed dict classes pickleable.
|
# Setting correct module is necessary to make typed dict classes pickleable.
|
||||||
ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__')
|
ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__')
|
||||||
except (AttributeError, ValueError):
|
except (AttributeError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return _TypedDictMeta(_typename, (), ns)
|
return _TypedDictMeta(typename, (), ns)
|
||||||
|
_typeddict_new.__text_signature__ = '($cls, _typename, _fields=None, /, *, total=True, **kwargs)'
|
||||||
|
|
||||||
|
|
||||||
def _check_fails(cls, other):
|
def _check_fails(cls, other):
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Constructors of :class:`~typing.NamedTuple` and :class:`~typing.TypedDict`
|
||||||
|
types now accept arbitrary keyword argument names, including "cls", "self",
|
||||||
|
"typename", "_typename", "fields" and "_fields". Passing positional
|
||||||
|
arguments by keyword is deprecated.
|
Loading…
Reference in New Issue