[Python-checkins] bpo-38191: Accept arbitrary keyword names in NamedTuple() and TypedDict(). (GH-16222)

Serhiy Storchaka webhook-mailer at python.org
Tue Sep 17 14:22:05 EDT 2019


https://github.com/python/cpython/commit/2bf31ccab3d17f3f35b42dca97f99576dfe2fc7d
commit: 2bf31ccab3d17f3f35b42dca97f99576dfe2fc7d
branch: master
author: Serhiy Storchaka <storchaka at gmail.com>
committer: GitHub <noreply at github.com>
date: 2019-09-17T21:22:00+03:00
summary:

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.

files:
A Misc/NEWS.d/next/Library/2019-09-17-12-28-27.bpo-38191.1TU0HV.rst
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 59b1b4e6d548..5914f314db0a 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -3563,6 +3563,36 @@ def test_namedtuple_keyword_usage(self):
         with self.assertRaises(TypeError):
             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):
         global Emp  # pickle wants to reference the class by name
         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.__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):
         Emp = TypedDict('Emp', {'name': str, 'id': int})
         self.assertEqual(TypedDict.__module__, 'typing')
diff --git a/Lib/typing.py b/Lib/typing.py
index 32011332127f..43486a7c8ba8 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -1653,35 +1653,96 @@ class Employee(NamedTuple):
     """
     _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:
             fields = kwargs.items()
         elif kwargs:
             raise TypeError("Either list of fields or keywords"
                             " can be provided to NamedTuple, not both")
         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)
-
-
-def _typeddict_new(cls, _typename, _fields=None, **kwargs):
-    total = kwargs.pop('total', True)
-    if _fields is None:
-        _fields = kwargs
+_dict_new.__text_signature__ = '($cls, _typename, _fields=None, /, **kwargs)'
+
+
+def _typeddict_new(*args, total=True, **kwargs):
+    if not args:
+        raise TypeError('TypedDict.__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("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:
         raise TypeError("TypedDict takes either a dict or keyword arguments,"
                         " but not both")
 
-    ns = {'__annotations__': dict(_fields), '__total__': total}
+    ns = {'__annotations__': dict(fields), '__total__': total}
     try:
         # Setting correct module is necessary to make typed dict classes pickleable.
         ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__')
     except (AttributeError, ValueError):
         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):
diff --git a/Misc/NEWS.d/next/Library/2019-09-17-12-28-27.bpo-38191.1TU0HV.rst b/Misc/NEWS.d/next/Library/2019-09-17-12-28-27.bpo-38191.1TU0HV.rst
new file mode 100644
index 000000000000..1a6de60e46c1
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2019-09-17-12-28-27.bpo-38191.1TU0HV.rst
@@ -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.



More information about the Python-checkins mailing list