Skip to content

Commit

Permalink
Fixed required/optional keys with old-style TypedDict (#778)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex Grönholm <[email protected]>
  • Loading branch information
domdfcoding and agronholm authored Apr 11, 2021
1 parent 6a2a490 commit 40932e3
Show file tree
Hide file tree
Showing 2 changed files with 15 additions and 4 deletions.
6 changes: 5 additions & 1 deletion typing_extensions/src_py3/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1530,7 +1530,7 @@ def test_typeddict_create_errors(self):

def test_typeddict_errors(self):
Emp = TypedDict('Emp', {'name': str, 'id': int})
if sys.version_info[:2] >= (3, 9):
if sys.version_info >= (3, 9, 2):
self.assertEqual(TypedDict.__module__, 'typing')
else:
self.assertEqual(TypedDict.__module__, 'typing_extensions')
Expand Down Expand Up @@ -1586,11 +1586,15 @@ def test_total(self):
self.assertEqual(D(), {})
self.assertEqual(D(x=1), {'x': 1})
self.assertEqual(D.__total__, False)
self.assertEqual(D.__required_keys__, frozenset())
self.assertEqual(D.__optional_keys__, {'x'})

if PY36:
self.assertEqual(Options(), {})
self.assertEqual(Options(log_level=2), {'log_level': 2})
self.assertEqual(Options.__total__, False)
self.assertEqual(Options.__required_keys__, frozenset())
self.assertEqual(Options.__optional_keys__, {'log_level', 'log_path'})

@skipUnless(PY36, 'Python 3.6 required')
def test_optional_keys(self):
Expand Down
13 changes: 10 additions & 3 deletions typing_extensions/src_py3/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1618,9 +1618,11 @@ def __index__(self) -> int:
pass


if sys.version_info[:2] >= (3, 9):
if sys.version_info >= (3, 9, 2):
# The standard library TypedDict in Python 3.8 does not store runtime information
# about which (if any) keys are optional. See https://bugs.python.org/issue38834
# The standard library TypedDict in Python 3.9.0/1 does not honour the "total"
# keyword with old-style TypedDict(). See https://bugs.python.org/issue42059
TypedDict = typing.TypedDict
else:
def _check_fails(cls, other):
Expand Down Expand Up @@ -1677,19 +1679,24 @@ def _typeddict_new(*args, total=True, **kwargs):
raise TypeError("TypedDict takes either a dict or keyword arguments,"
" but not both")

ns = {'__annotations__': dict(fields), '__total__': total}
ns = {'__annotations__': dict(fields)}
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, total=total)

_typeddict_new.__text_signature__ = ('($cls, _typename, _fields=None,'
' /, *, total=True, **kwargs)')

class _TypedDictMeta(type):
def __init__(cls, name, bases, ns, total=True):
# In Python 3.4 and 3.5 the __init__ method also needs to support the keyword arguments.
# See https://www.python.org/dev/peps/pep-0487/#implementation-details
super(_TypedDictMeta, cls).__init__(name, bases, ns)

def __new__(cls, name, bases, ns, total=True):
# Create new typed dict class object.
# This method is called directly when TypedDict is subclassed,
Expand Down

0 comments on commit 40932e3

Please sign in to comment.