Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2bf31cc

Browse files
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.
1 parent b574813 commit 2bf31cc

3 files changed

Lines changed: 135 additions & 10 deletions

File tree

Lib/test/test_typing.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3563,6 +3563,36 @@ def test_namedtuple_keyword_usage(self):
35633563
with self.assertRaises(TypeError):
35643564
NamedTuple('Name', x=1, y='a')
35653565

3566+
def test_namedtuple_special_keyword_names(self):
3567+
NT = NamedTuple("NT", cls=type, self=object, typename=str, fields=list)
3568+
self.assertEqual(NT.__name__, 'NT')
3569+
self.assertEqual(NT._fields, ('cls', 'self', 'typename', 'fields'))
3570+
a = NT(cls=str, self=42, typename='foo', fields=[('bar', tuple)])
3571+
self.assertEqual(a.cls, str)
3572+
self.assertEqual(a.self, 42)
3573+
self.assertEqual(a.typename, 'foo')
3574+
self.assertEqual(a.fields, [('bar', tuple)])
3575+
3576+
def test_namedtuple_errors(self):
3577+
with self.assertRaises(TypeError):
3578+
NamedTuple.__new__()
3579+
with self.assertRaises(TypeError):
3580+
NamedTuple()
3581+
with self.assertRaises(TypeError):
3582+
NamedTuple('Emp', [('name', str)], None)
3583+
with self.assertRaises(ValueError):
3584+
NamedTuple('Emp', [('_name', str)])
3585+
3586+
with self.assertWarns(DeprecationWarning):
3587+
Emp = NamedTuple(typename='Emp', name=str, id=int)
3588+
self.assertEqual(Emp.__name__, 'Emp')
3589+
self.assertEqual(Emp._fields, ('name', 'id'))
3590+
3591+
with self.assertWarns(DeprecationWarning):
3592+
Emp = NamedTuple('Emp', fields=[('name', str), ('id', int)])
3593+
self.assertEqual(Emp.__name__, 'Emp')
3594+
self.assertEqual(Emp._fields, ('name', 'id'))
3595+
35663596
def test_pickle(self):
35673597
global Emp # pickle wants to reference the class by name
35683598
Emp = NamedTuple('Emp', [('name', str), ('id', int)])
@@ -3604,6 +3634,36 @@ def test_basics_keywords_syntax(self):
36043634
self.assertEqual(Emp.__annotations__, {'name': str, 'id': int})
36053635
self.assertEqual(Emp.__total__, True)
36063636

3637+
def test_typeddict_special_keyword_names(self):
3638+
TD = TypedDict("TD", cls=type, self=object, typename=str, _typename=int, fields=list, _fields=dict)
3639+
self.assertEqual(TD.__name__, 'TD')
3640+
self.assertEqual(TD.__annotations__, {'cls': type, 'self': object, 'typename': str, '_typename': int, 'fields': list, '_fields': dict})
3641+
a = TD(cls=str, self=42, typename='foo', _typename=53, fields=[('bar', tuple)], _fields={'baz', set})
3642+
self.assertEqual(a['cls'], str)
3643+
self.assertEqual(a['self'], 42)
3644+
self.assertEqual(a['typename'], 'foo')
3645+
self.assertEqual(a['_typename'], 53)
3646+
self.assertEqual(a['fields'], [('bar', tuple)])
3647+
self.assertEqual(a['_fields'], {'baz', set})
3648+
3649+
def test_typeddict_create_errors(self):
3650+
with self.assertRaises(TypeError):
3651+
TypedDict.__new__()
3652+
with self.assertRaises(TypeError):
3653+
TypedDict()
3654+
with self.assertRaises(TypeError):
3655+
TypedDict('Emp', [('name', str)], None)
3656+
3657+
with self.assertWarns(DeprecationWarning):
3658+
Emp = TypedDict(_typename='Emp', name=str, id=int)
3659+
self.assertEqual(Emp.__name__, 'Emp')
3660+
self.assertEqual(Emp.__annotations__, {'name': str, 'id': int})
3661+
3662+
with self.assertWarns(DeprecationWarning):
3663+
Emp = TypedDict('Emp', _fields={'name': str, 'id': int})
3664+
self.assertEqual(Emp.__name__, 'Emp')
3665+
self.assertEqual(Emp.__annotations__, {'name': str, 'id': int})
3666+
36073667
def test_typeddict_errors(self):
36083668
Emp = TypedDict('Emp', {'name': str, 'id': int})
36093669
self.assertEqual(TypedDict.__module__, 'typing')

Lib/typing.py

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1653,35 +1653,96 @@ class Employee(NamedTuple):
16531653
"""
16541654
_root = True
16551655

1656-
def __new__(self, typename, fields=None, **kwargs):
1656+
def __new__(*args, **kwargs):
1657+
if not args:
1658+
raise TypeError('NamedTuple.__new__(): not enough arguments')
1659+
cls, *args = args # allow the "cls" keyword be passed
1660+
if args:
1661+
typename, *args = args # allow the "typename" keyword be passed
1662+
elif 'typename' in kwargs:
1663+
typename = kwargs.pop('typename')
1664+
import warnings
1665+
warnings.warn("Passing 'typename' as keyword argument is deprecated",
1666+
DeprecationWarning, stacklevel=2)
1667+
else:
1668+
raise TypeError("NamedTuple.__new__() missing 1 required positional "
1669+
"argument: 'typename'")
1670+
if args:
1671+
try:
1672+
fields, = args # allow the "fields" keyword be passed
1673+
except ValueError:
1674+
raise TypeError(f'NamedTuple.__new__() takes from 2 to 3 '
1675+
f'positional arguments but {len(args) + 2} '
1676+
f'were given') from None
1677+
elif 'fields' in kwargs and len(kwargs) == 1:
1678+
fields = kwargs.pop('fields')
1679+
import warnings
1680+
warnings.warn("Passing 'fields' as keyword argument is deprecated",
1681+
DeprecationWarning, stacklevel=2)
1682+
else:
1683+
fields = None
1684+
16571685
if fields is None:
16581686
fields = kwargs.items()
16591687
elif kwargs:
16601688
raise TypeError("Either list of fields or keywords"
16611689
" can be provided to NamedTuple, not both")
16621690
return _make_nmtuple(typename, fields)
1691+
__new__.__text_signature__ = '($cls, typename, fields=None, /, **kwargs)'
16631692

16641693

1665-
def _dict_new(cls, *args, **kwargs):
1694+
def _dict_new(*args, **kwargs):
1695+
if not args:
1696+
raise TypeError('TypedDict.__new__(): not enough arguments')
1697+
cls, *args = args # allow the "cls" keyword be passed
16661698
return dict(*args, **kwargs)
1667-
1668-
1669-
def _typeddict_new(cls, _typename, _fields=None, **kwargs):
1670-
total = kwargs.pop('total', True)
1671-
if _fields is None:
1672-
_fields = kwargs
1699+
_dict_new.__text_signature__ = '($cls, _typename, _fields=None, /, **kwargs)'
1700+
1701+
1702+
def _typeddict_new(*args, total=True, **kwargs):
1703+
if not args:
1704+
raise TypeError('TypedDict.__new__(): not enough arguments')
1705+
cls, *args = args # allow the "cls" keyword be passed
1706+
if args:
1707+
typename, *args = args # allow the "_typename" keyword be passed
1708+
elif '_typename' in kwargs:
1709+
typename = kwargs.pop('_typename')
1710+
import warnings
1711+
warnings.warn("Passing '_typename' as keyword argument is deprecated",
1712+
DeprecationWarning, stacklevel=2)
1713+
else:
1714+
raise TypeError("TypedDict.__new__() missing 1 required positional "
1715+
"argument: '_typename'")
1716+
if args:
1717+
try:
1718+
fields, = args # allow the "_fields" keyword be passed
1719+
except ValueError:
1720+
raise TypeError(f'TypedDict.__new__() takes from 2 to 3 '
1721+
f'positional arguments but {len(args) + 2} '
1722+
f'were given') from None
1723+
elif '_fields' in kwargs and len(kwargs) == 1:
1724+
fields = kwargs.pop('_fields')
1725+
import warnings
1726+
warnings.warn("Passing '_fields' as keyword argument is deprecated",
1727+
DeprecationWarning, stacklevel=2)
1728+
else:
1729+
fields = None
1730+
1731+
if fields is None:
1732+
fields = kwargs
16731733
elif kwargs:
16741734
raise TypeError("TypedDict takes either a dict or keyword arguments,"
16751735
" but not both")
16761736

1677-
ns = {'__annotations__': dict(_fields), '__total__': total}
1737+
ns = {'__annotations__': dict(fields), '__total__': total}
16781738
try:
16791739
# Setting correct module is necessary to make typed dict classes pickleable.
16801740
ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__')
16811741
except (AttributeError, ValueError):
16821742
pass
16831743

1684-
return _TypedDictMeta(_typename, (), ns)
1744+
return _TypedDictMeta(typename, (), ns)
1745+
_typeddict_new.__text_signature__ = '($cls, _typename, _fields=None, /, *, total=True, **kwargs)'
16851746

16861747

16871748
def _check_fails(cls, other):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Constructors of :class:`~typing.NamedTuple` and :class:`~typing.TypedDict`
2+
types now accept arbitrary keyword argument names, including "cls", "self",
3+
"typename", "_typename", "fields" and "_fields". Passing positional
4+
arguments by keyword is deprecated.

0 commit comments

Comments
 (0)