Skip to content

Deprecate mypy_extensions.TypedDict #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 28, 2023
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
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