Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ jobs:
"3.11",
"3.12",
"3.13",
"3.14",
]

steps:
Expand Down
8 changes: 6 additions & 2 deletions mypy_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,15 @@ def __new__(cls, name, bases, ns, total=True, _from_functional_call=False):
ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new
tp_dict = super(_TypedDictMeta, cls).__new__(cls, name, (dict,), ns)

anns = ns.get('__annotations__', {})
if sys.version_info >= (3, 14):
import annotationlib
anns = annotationlib.get_annotations(tp_dict, format=annotationlib.Format.FORWARDREF)
else:
anns = ns.get('__annotations__', {})
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
anns = {n: _type_check(tp, msg) for n, tp in anns.items()}
for base in bases:
anns.update(base.__dict__.get('__annotations__', {}))
anns.update(getattr(base, '__annotations__', {}))
tp_dict.__annotations__ = anns
if not hasattr(tp_dict, '__total__'):
tp_dict.__total__ = total
Expand Down
47 changes: 47 additions & 0 deletions tests/testextensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,53 @@ def test_py36_class_syntax_usage(self):
other = LabelPoint2D(x=0, y=1, label='hi') # noqa
self.assertEqual(other['label'], 'hi')

def test_class_annotations(self):
LocalType = int
with self.assert_typeddict_deprecated():
class Local(TypedDict):
value: LocalType

self.assertEqual(Local.__annotations__, {'value': int})
with self.assert_typeddict_deprecated():
class Child(Local):
label: str

self.assertEqual(Child.__annotations__, {'value': int, 'label': str})
self.assertEqual(Local.__annotations__, {'value': int})

def test_class_invalid_annotation(self):
with self.assertRaises(TypeError), self.assert_typeddict_deprecated():
class Invalid(TypedDict):
value: ()

def test_class_forward_reference(self):
with self.assert_typeddict_deprecated():
class Forward(TypedDict):
value: 'Later'

class Later:
pass

self.assertEqual(typing.get_type_hints(Forward, localns={'Later': Later}),
{'value': Later})

def test_class_deferred_self_reference(self):
if sys.version_info < (3, 14):
self.skipTest('requires deferred annotations')
namespace = {'TypedDict': TypedDict}
with self.assert_typeddict_deprecated():
exec('class Node(TypedDict):\n child: Node', namespace)
Node = namespace['Node']
self.assertEqual(typing.get_type_hints(Node, globalns=namespace), {'child': Node})

def test_class_future_annotations(self):
namespace = {'TypedDict': TypedDict}
with self.assert_typeddict_deprecated():
exec('from __future__ import annotations\n'
'class Future(TypedDict):\n value: int', namespace)
self.assertEqual(typing.get_type_hints(namespace['Future'], globalns=namespace),
{'value': int})

def test_py36_class_usage_emits_deprecations(self):
with self.assert_typeddict_deprecated():
class Foo(TypedDict):
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tox]
minversion = 4.4.4
skip_missing_interpreters = true
envlist = py38, py39, py310, py311, py312, py313
envlist = py38, py39, py310, py311, py312, py313, py314

[testenv]
description = run the test driver with {basepython}
Expand Down
Loading