Skip to content

Commit

Permalink
Deprecate mypy_extensions.TypedDict (#47)
Browse files Browse the repository at this point in the history
* Deprecate `mypy_extensions.TypedDict`

* Formatting
  • Loading branch information
AlexWaygood authored Aug 28, 2023
1 parent a1b59f7 commit e0c6670
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 19 deletions.
19 changes: 17 additions & 2 deletions mypy_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,32 @@ def _typeddict_new(cls, _typename, _fields=None, **kwargs):
except (AttributeError, ValueError):
pass

return _TypedDictMeta(_typename, (), ns)
return _TypedDictMeta(_typename, (), ns, _from_functional_call=True)


class _TypedDictMeta(type):
def __new__(cls, name, bases, ns, total=True):
def __new__(cls, name, bases, ns, total=True, _from_functional_call=False):
# Create new typed dict class object.
# This method is called directly when TypedDict is subclassed,
# or via _typeddict_new when TypedDict is instantiated. This way
# TypedDict supports all three syntaxes described in its docstring.
# Subclasses and instances of TypedDict return actual dictionaries
# via _dict_new.

# We need the `if TypedDict in globals()` check,
# or we emit a DeprecationWarning when creating mypy_extensions.TypedDict itself
if 'TypedDict' in globals():
import warnings
warnings.warn(
(
"mypy_extensions.TypedDict is deprecated, "
"and will be removed in a future version. "
"Use typing.TypedDict or typing_extensions.TypedDict instead."
),
DeprecationWarning,
stacklevel=(3 if _from_functional_call else 2)
)

ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new
tp_dict = super(_TypedDictMeta, cls).__new__(cls, name, (dict,), ns)

Expand Down
61 changes: 45 additions & 16 deletions tests/testextensions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import sys
import pickle
import typing
from contextlib import contextmanager
from textwrap import dedent
from unittest import TestCase, main, skipUnless
from mypy_extensions import TypedDict, i64, i32, i16, u8

Expand All @@ -25,27 +27,39 @@ def assertNotIsSubclass(self, cls, class_or_tuple, msg=None):
PY36 = sys.version_info[:2] >= (3, 6)

PY36_TESTS = """
Label = TypedDict('Label', [('label', str)])
import warnings
class Point2D(TypedDict):
x: int
y: int
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=DeprecationWarning)
class LabelPoint2D(Point2D, Label): ...
Label = TypedDict('Label', [('label', str)])
class Options(TypedDict, total=False):
log_level: int
log_path: str
class Point2D(TypedDict):
x: int
y: int
class LabelPoint2D(Point2D, Label): ...
class Options(TypedDict, total=False):
log_level: int
log_path: str
"""

if PY36:
exec(PY36_TESTS)


class TypedDictTests(BaseTestCase):
@contextmanager
def assert_typeddict_deprecated(self):
with self.assertWarnsRegex(
DeprecationWarning, "mypy_extensions.TypedDict is deprecated"
):
yield

def test_basics_iterable_syntax(self):
Emp = TypedDict('Emp', {'name': str, 'id': int})
with self.assert_typeddict_deprecated():
Emp = TypedDict('Emp', {'name': str, 'id': int})
self.assertIsSubclass(Emp, dict)
self.assertIsSubclass(Emp, typing.MutableMapping)
if sys.version_info[0] >= 3:
Expand All @@ -62,7 +76,8 @@ def test_basics_iterable_syntax(self):
self.assertEqual(Emp.__total__, True)

def test_basics_keywords_syntax(self):
Emp = TypedDict('Emp', name=str, id=int)
with self.assert_typeddict_deprecated():
Emp = TypedDict('Emp', name=str, id=int)
self.assertIsSubclass(Emp, dict)
self.assertIsSubclass(Emp, typing.MutableMapping)
if sys.version_info[0] >= 3:
Expand All @@ -79,7 +94,8 @@ def test_basics_keywords_syntax(self):
self.assertEqual(Emp.__total__, True)

def test_typeddict_errors(self):
Emp = TypedDict('Emp', {'name': str, 'id': int})
with self.assert_typeddict_deprecated():
Emp = TypedDict('Emp', {'name': str, 'id': int})
self.assertEqual(TypedDict.__module__, 'mypy_extensions')
jim = Emp(name='Jim', id=1)
with self.assertRaises(TypeError):
Expand All @@ -88,9 +104,9 @@ def test_typeddict_errors(self):
isinstance(jim, Emp) # type: ignore
with self.assertRaises(TypeError):
issubclass(dict, Emp) # type: ignore
with self.assertRaises(TypeError):
with self.assertRaises(TypeError), self.assert_typeddict_deprecated():
TypedDict('Hi', x=())
with self.assertRaises(TypeError):
with self.assertRaises(TypeError), self.assert_typeddict_deprecated():
TypedDict('Hi', [('x', int), ('y', ())])
with self.assertRaises(TypeError):
TypedDict('Hi', [('x', int)], y=int)
Expand All @@ -109,9 +125,20 @@ def test_py36_class_syntax_usage(self):
other = LabelPoint2D(x=0, y=1, label='hi') # noqa
self.assertEqual(other['label'], 'hi')

if PY36:
exec(dedent(
"""
def test_py36_class_usage_emits_deprecations(self):
with self.assert_typeddict_deprecated():
class Foo(TypedDict):
bar: int
"""
))

def test_pickle(self):
global EmpD # pickle wants to reference the class by name
EmpD = TypedDict('EmpD', name=str, id=int)
with self.assert_typeddict_deprecated():
EmpD = TypedDict('EmpD', name=str, id=int)
jane = EmpD({'name': 'jane', 'id': 37})
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
z = pickle.dumps(jane, proto)
Expand All @@ -123,13 +150,15 @@ def test_pickle(self):
self.assertEqual(EmpDnew({'name': 'jane', 'id': 37}), jane)

def test_optional(self):
EmpD = TypedDict('EmpD', name=str, id=int)
with self.assert_typeddict_deprecated():
EmpD = TypedDict('EmpD', name=str, id=int)

self.assertEqual(typing.Optional[EmpD], typing.Union[None, EmpD])
self.assertNotEqual(typing.List[EmpD], typing.Tuple[EmpD])

def test_total(self):
D = TypedDict('D', {'x': int}, total=False)
with self.assert_typeddict_deprecated():
D = TypedDict('D', {'x': int}, total=False)
self.assertEqual(D(), {})
self.assertEqual(D(x=1), {'x': 1})
self.assertEqual(D.__total__, False)
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ envlist = py35, py36, py37, py38, py39, py310, py311

[testenv]
description = run the test driver with {basepython}
commands = python -m unittest discover tests
commands = python -We -m unittest discover tests

[testenv:lint]
description = check the code style
Expand Down

0 comments on commit e0c6670

Please sign in to comment.