From e0c6670e05a87507d59b7d3a0aa2eec88e9813b0 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 28 Aug 2023 14:35:01 +0100 Subject: [PATCH] Deprecate `mypy_extensions.TypedDict` (#47) * Deprecate `mypy_extensions.TypedDict` * Formatting --- mypy_extensions.py | 19 +++++++++++-- tests/testextensions.py | 61 ++++++++++++++++++++++++++++++----------- tox.ini | 2 +- 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/mypy_extensions.py b/mypy_extensions.py index 6600b21..aff5145 100644 --- a/mypy_extensions.py +++ b/mypy_extensions.py @@ -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) diff --git a/tests/testextensions.py b/tests/testextensions.py index 991c4e5..a41f5f6 100644 --- a/tests/testextensions.py +++ b/tests/testextensions.py @@ -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 @@ -25,17 +27,22 @@ 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: @@ -43,9 +50,16 @@ class Options(TypedDict, total=False): 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: @@ -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: @@ -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): @@ -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) @@ -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) @@ -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) diff --git a/tox.ini b/tox.ini index 9f64a32..b0766b5 100644 --- a/tox.ini +++ b/tox.ini @@ -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