Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Implement support for PEP 764 (inline typed dictionaries) #579

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

Closed
wants to merge 2 commits into from
Closed
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
38 changes: 37 additions & 1 deletion src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5066,6 +5066,42 @@ def test_cannot_combine_closed_and_extra_items(self):
class TD(TypedDict, closed=True, extra_items=range):
x: str

def test_inlined_too_many_arguments(self):
with self.assertRaises(TypeError):
TypedDict[{"a": int}, "extra"]

def test_inlined_not_a_dict(self):
with self.assertRaises(TypeError):
TypedDict["not_a_dict"]

def test_inlined_empty(self):
TD = TypedDict[{}]
self.assertEqual(TD.__required_keys__, set())

def test_inlined(self):
TD = TypedDict[{
"a": int,
"b": Required[int],
"c": NotRequired[int],
"d": ReadOnly[int],
}]
self.assertIsSubclass(TD, dict)
self.assertIsSubclass(TD, typing.MutableMapping)
self.assertNotIsSubclass(TD, collections.abc.Sequence)
self.assertTrue(is_typeddict(TD))
self.assertEqual(TD.__name__, "<inlined TypedDict>")
self.assertEqual(TD.__module__, __name__)
self.assertEqual(TD.__bases__, (dict,))
self.assertEqual(TD.__total__, True)
self.assertEqual(TD.__required_keys__, {"a", "b", "d"})
self.assertEqual(TD.__optional_keys__, {"c"})
self.assertEqual(TD.__readonly_keys__, {"d"})
self.assertEqual(TD.__mutable_keys__, {"a", "b", "c"})

inst = TD(a=1, b=2, d=3)
self.assertIs(type(inst), dict)
self.assertEqual(inst["a"], 1)


class AnnotatedTests(BaseTestCase):

Expand Down Expand Up @@ -6629,7 +6665,7 @@ def test_typing_extensions_defers_when_possible(self):
exclude |= {
'TypeAliasType'
}
if not typing_extensions._PEP_728_IMPLEMENTED:
if not typing_extensions._PEP_728_OR_764_IMPLEMENTED:
exclude |= {'TypedDict', 'is_typeddict'}
for item in typing_extensions.__all__:
if item not in exclude and hasattr(typing, item):
Expand Down
145 changes: 84 additions & 61 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -910,11 +910,11 @@ def __reduce__(self):
del SingletonMeta


# Update this to something like >=3.13.0b1 if and when
# PEP 728 is implemented in CPython
_PEP_728_IMPLEMENTED = False
# Update this to something like >=3.14 if and when
# PEP 728/PEP 764 is implemented in CPython
_PEP_728_OR_764_IMPLEMENTED = False

if _PEP_728_IMPLEMENTED:
if _PEP_728_OR_764_IMPLEMENTED:
# 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
# The standard library TypedDict below Python 3.11 does not store runtime
Expand All @@ -924,7 +924,7 @@ def __reduce__(self):
# to enable better runtime introspection.
# On 3.13 we deprecate some odd ways of creating TypedDicts.
# Also on 3.13, PEP 705 adds the ReadOnly[] qualifier.
# PEP 728 (still pending) makes more changes.
# PEP 728 and PEP 764 (still pending) makes more changes.
TypedDict = typing.TypedDict
_TypedDictMeta = typing._TypedDictMeta
is_typeddict = typing.is_typeddict
Expand Down Expand Up @@ -1068,7 +1068,11 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None,
tp_dict.__extra_items__ = extra_items_type
return tp_dict

__call__ = dict # static method
def __call__(cls, /, *args, **kwargs):
if cls is TypedDict:
# Functional syntax, let `TypedDict.__new__` handle it:
return super().__call__(*args, **kwargs)
return dict(*args, **kwargs)

def __subclasscheck__(cls, other):
# Typed dicts are only for static structural subtyping.
Expand All @@ -1078,17 +1082,7 @@ def __subclasscheck__(cls, other):

_TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {})

@_ensure_subclassable(lambda bases: (_TypedDict,))
def TypedDict(
typename,
fields=_marker,
/,
*,
total=True,
closed=None,
extra_items=NoExtraItems,
**kwargs
):
class TypedDict(metaclass=_TypedDictMeta):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose it's necessary to make TypedDict a class so that you can subscript it? What about making it an instance of a _SpecialForm subclass, similar to how Annotated is implemented in CPython?

I doubt there's much difference between the two in terms of the user-facing behaviour, but conceptually it sort-of feels "wrong" for TypedDict to be a class. You use TypedDict to create types; it is not in itself a type; calling TypedDict does not create an "instance of TypedDict"; x: TypedDict is an invalid type annotation; etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll give it a try.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened #580 to compare both.

"""A simple typed namespace. At runtime it is equivalent to a plain dict.

TypedDict creates a dictionary type such that a type checker will expect all
Expand Down Expand Up @@ -1135,52 +1129,77 @@ class Point2D(TypedDict):

See PEP 655 for more details on Required and NotRequired.
"""
if fields is _marker or fields is None:
if fields is _marker:
deprecated_thing = "Failing to pass a value for the 'fields' parameter"
else:
deprecated_thing = "Passing `None` as the 'fields' parameter"

example = f"`{typename} = TypedDict({typename!r}, {{}})`"
deprecation_msg = (
f"{deprecated_thing} is deprecated and will be disallowed in "
"Python 3.15. To create a TypedDict class with 0 fields "
"using the functional syntax, pass an empty dictionary, e.g. "
) + example + "."
warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2)
# Support a field called "closed"
if closed is not False and closed is not True and closed is not None:
kwargs["closed"] = closed
closed = None
# Or "extra_items"
if extra_items is not NoExtraItems:
kwargs["extra_items"] = extra_items
extra_items = NoExtraItems
fields = kwargs
elif kwargs:
raise TypeError("TypedDict takes either a dict or keyword arguments,"
" but not both")
if kwargs:
if sys.version_info >= (3, 13):
raise TypeError("TypedDict takes no keyword arguments")
warnings.warn(
"The kwargs-based syntax for TypedDict definitions is deprecated "
"in Python 3.11, will be removed in Python 3.13, and may not be "
"understood by third-party type checkers.",
DeprecationWarning,
stacklevel=2,
)
def __new__(
cls,
typename,
fields=_marker,
/,
*,
total=True,
closed=None,
extra_items=NoExtraItems,
**kwargs
):
if fields is _marker or fields is None:
if fields is _marker:
deprecated_thing = (
"Failing to pass a value for the 'fields' parameter"
)
else:
deprecated_thing = "Passing `None` as the 'fields' parameter"

example = f"`{typename} = TypedDict({typename!r}, {{}})`"
deprecation_msg = (
f"{deprecated_thing} is deprecated and will be disallowed in "
"Python 3.15. To create a TypedDict class with 0 fields "
"using the functional syntax, pass an empty dictionary, e.g. "
) + example + "."
warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2)
# Support a field called "closed"
if closed is not False and closed is not True and closed is not None:
kwargs["closed"] = closed
closed = None
# Or "extra_items"
if extra_items is not NoExtraItems:
kwargs["extra_items"] = extra_items
extra_items = NoExtraItems
fields = kwargs
elif kwargs:
raise TypeError("TypedDict takes either a dict or keyword arguments,"
" but not both")
if kwargs:
if sys.version_info >= (3, 13):
raise TypeError("TypedDict takes no keyword arguments")
warnings.warn(
"The kwargs-based syntax for TypedDict definitions is deprecated "
"in Python 3.11, will be removed in Python 3.13, and may not be "
"understood by third-party type checkers.",
DeprecationWarning,
stacklevel=2,
)

ns = {'__annotations__': dict(fields)}
module = _caller(depth=3)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be great to have _caller() match the upstream, in particular having python/cpython#99520. Let me know and I'll send a separate PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please do! Seems like it would have to be version-guarded.

if module is not None:
# Setting correct module is necessary to make typed dict classes
# pickleable.
ns['__module__'] = module

ns = {'__annotations__': dict(fields)}
module = _caller()
if module is not None:
# Setting correct module is necessary to make typed dict classes pickleable.
ns['__module__'] = module
td = _TypedDictMeta(typename, (), ns, total=total, closed=closed,
extra_items=extra_items)
td.__orig_bases__ = (TypedDict,)
return td

td = _TypedDictMeta(typename, (), ns, total=total, closed=closed,
extra_items=extra_items)
td.__orig_bases__ = (TypedDict,)
return td
def __class_getitem__(cls, args):
if not isinstance(args, tuple):
args = (args,)
if len(args) != 1 or not isinstance(args[0], dict):
raise TypeError(
"TypedDict[...] should be used with a single dict argument"
)

return cls.__new__(cls, "<inlined TypedDict>", args[0])

_TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta)

Expand All @@ -1195,7 +1214,11 @@ class Film(TypedDict):
is_typeddict(Film) # => True
is_typeddict(Union[list, str]) # => False
"""
return isinstance(tp, _TYPEDDICT_TYPES)
return (
tp is not TypedDict
and tp is not typing.TypedDict
and isinstance(tp, _TYPEDDICT_TYPES)
)


if hasattr(typing, "assert_type"):
Expand Down
Loading