Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2513,6 +2513,38 @@ def test_namedtuple_keyword_usage(self):
with self.assertRaises(TypeError):
NamedTuple('Name', x=1, y='a')

@skipUnless(PY36, 'Python 3.6 required')
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)])

@skipUnless(PY36, 'Python 3.6 required')
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)])
Expand Down
32 changes: 31 additions & 1 deletion src/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2204,17 +2204,47 @@ class Employee(NamedTuple):
"""
_root = True

def __new__(self, typename, fields=None, **kwargs):
def __new__(*args, **kwargs):
if kwargs and not _PY36:
raise TypeError("Keyword syntax for NamedTuple is only supported"
" in Python 3.6+")
if not args:
raise TypeError('NamedTuple.__new__(): not enough arguments')
_, args = args[0], args[1:] # allow the "cls" keyword be passed
if args:
typename, args = args[0], args[1:] # 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('NamedTuple.__new__() takes from 2 to 3 '
'positional arguments but {} '
'were given'.format(len(args) + 2))
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 NewType(name, tp):
"""NewType creates simple unique types with almost zero
Expand Down
36 changes: 35 additions & 1 deletion typing_extensions/src_py3/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pickle
import subprocess
import types
from unittest import TestCase, main, skipUnless
from unittest import TestCase, main, skipUnless, skipIf
from typing import TypeVar, Optional
from typing import T, KT, VT # Not in __all__.
from typing import Tuple, List, Dict, Iterator
Expand Down Expand Up @@ -1445,6 +1445,40 @@ 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})

@skipIf(hasattr(typing, 'TypedDict'), "Should be tested by upstream")
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})
if hasattr(typing, 'TypedDict'):
Expand Down
49 changes: 42 additions & 7 deletions typing_extensions/src_py3/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1583,25 +1583,60 @@ def _check_fails(cls, other):
pass
return False

def _dict_new(cls, *args, **kwargs):
def _dict_new(*args, **kwargs):
if not args:
raise TypeError('TypedDict.__new__(): not enough arguments')
_, args = args[0], args[1:] # 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')
_, args = args[0], args[1:] # allow the "cls" keyword be passed
if args:
typename, args = args[0], args[1:] # 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('TypedDict.__new__() takes from 2 to 3 '
'positional arguments but {} '
'were given'.format(len(args) + 2))
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)')

class _TypedDictMeta(type):
def __new__(cls, name, bases, ns, total=True):
Expand Down