-
-
Notifications
You must be signed in to change notification settings - Fork 111
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
Backport NamedTuple and TypedDict deprecations from Python 3.13 #240
Changes from all commits
1bc23d0
51fc6a7
0092208
4f2b790
f4adac5
1cc1897
3354680
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -972,7 +972,7 @@ def __round__(self, ndigits: int = 0) -> T_co: | |||||||||
pass | ||||||||||
|
||||||||||
|
||||||||||
if sys.version_info >= (3, 12): | ||||||||||
if sys.version_info >= (3, 13): | ||||||||||
# The standard library TypedDict in Python 3.8 does not store runtime information | ||||||||||
# about which (if any) keys are optional. See https://bugs.python.org/issue38834 | ||||||||||
# The standard library TypedDict in Python 3.9.0/1 does not honour the "total" | ||||||||||
|
@@ -982,6 +982,7 @@ def __round__(self, ndigits: int = 0) -> T_co: | |||||||||
# Generic TypedDicts are also impossible using typing.TypedDict on Python <3.11. | ||||||||||
# Aaaand on 3.12 we add __orig_bases__ to TypedDict | ||||||||||
# to enable better runtime introspection. | ||||||||||
# On 3.13 we deprecate some odd ways of creating TypedDicts. | ||||||||||
TypedDict = typing.TypedDict | ||||||||||
_TypedDictMeta = typing._TypedDictMeta | ||||||||||
is_typeddict = typing.is_typeddict | ||||||||||
|
@@ -1077,13 +1078,14 @@ def __subclasscheck__(cls, other): | |||||||||
|
||||||||||
__instancecheck__ = __subclasscheck__ | ||||||||||
|
||||||||||
def TypedDict(__typename, __fields=None, *, total=True, **kwargs): | ||||||||||
def TypedDict(__typename, __fields=_marker, *, total=True, **kwargs): | ||||||||||
"""A simple typed namespace. At runtime it is equivalent to a plain dict. | ||||||||||
|
||||||||||
TypedDict creates a dictionary type that expects all of its | ||||||||||
TypedDict creates a dictionary type such that a type checker will expect all | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I took this opportunity to copy-paste the docstring from CPython. |
||||||||||
instances to have a certain set of keys, where each key is | ||||||||||
associated with a value of a consistent type. This expectation | ||||||||||
is not checked at runtime but is only enforced by type checkers. | ||||||||||
is not checked at runtime. | ||||||||||
|
||||||||||
Usage:: | ||||||||||
|
||||||||||
class Point2D(TypedDict): | ||||||||||
|
@@ -1103,19 +1105,39 @@ class Point2D(TypedDict): | |||||||||
Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}) | ||||||||||
|
||||||||||
By default, all keys must be present in a TypedDict. It is possible | ||||||||||
to override this by specifying totality. | ||||||||||
Usage:: | ||||||||||
to override this by specifying totality:: | ||||||||||
|
||||||||||
class point2D(TypedDict, total=False): | ||||||||||
class Point2D(TypedDict, total=False): | ||||||||||
x: int | ||||||||||
y: int | ||||||||||
|
||||||||||
This means that a point2D TypedDict can have any of the keys omitted. A type | ||||||||||
This means that a Point2D TypedDict can have any of the keys omitted. A type | ||||||||||
checker is only expected to support a literal False or True as the value of | ||||||||||
the total argument. True is the default, and makes all items defined in the | ||||||||||
class body be required. | ||||||||||
|
||||||||||
The Required and NotRequired special forms can also be used to mark | ||||||||||
individual keys as being required or not required:: | ||||||||||
|
||||||||||
class Point2D(TypedDict): | ||||||||||
x: int # the "x" key must always be present (Required is the default) | ||||||||||
y: NotRequired[int] # the "y" key can be omitted | ||||||||||
|
||||||||||
See PEP 655 for more details on Required and NotRequired. | ||||||||||
""" | ||||||||||
if __fields is None: | ||||||||||
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) | ||||||||||
__fields = kwargs | ||||||||||
elif kwargs: | ||||||||||
raise TypeError("TypedDict takes either a dict or keyword arguments," | ||||||||||
|
@@ -2570,7 +2592,8 @@ def wrapper(*args, **kwargs): | |||||||||
# In 3.11, the ability to define generic `NamedTuple`s was supported. | ||||||||||
# This was explicitly disallowed in 3.9-3.10, and only half-worked in <=3.8. | ||||||||||
# On 3.12, we added __orig_bases__ to call-based NamedTuples | ||||||||||
if sys.version_info >= (3, 12): | ||||||||||
# On 3.13, we deprecated kwargs-based NamedTuples | ||||||||||
if sys.version_info >= (3, 13): | ||||||||||
NamedTuple = typing.NamedTuple | ||||||||||
else: | ||||||||||
def _make_nmtuple(name, types, module, defaults=()): | ||||||||||
|
@@ -2614,8 +2637,11 @@ def __new__(cls, typename, bases, ns): | |||||||||
) | ||||||||||
nm_tpl.__bases__ = bases | ||||||||||
if typing.Generic in bases: | ||||||||||
class_getitem = typing.Generic.__class_getitem__.__func__ | ||||||||||
nm_tpl.__class_getitem__ = classmethod(class_getitem) | ||||||||||
if hasattr(typing, '_generic_class_getitem'): # 3.12+ | ||||||||||
nm_tpl.__class_getitem__ = classmethod(typing._generic_class_getitem) | ||||||||||
else: | ||||||||||
class_getitem = typing.Generic.__class_getitem__.__func__ | ||||||||||
nm_tpl.__class_getitem__ = classmethod(class_getitem) | ||||||||||
# update from user namespace without overriding special namedtuple attributes | ||||||||||
for key in ns: | ||||||||||
if key in _prohibited_namedtuple_fields: | ||||||||||
|
@@ -2626,17 +2652,71 @@ def __new__(cls, typename, bases, ns): | |||||||||
nm_tpl.__init_subclass__() | ||||||||||
return nm_tpl | ||||||||||
|
||||||||||
def NamedTuple(__typename, __fields=None, **kwargs): | ||||||||||
if __fields is None: | ||||||||||
__fields = kwargs.items() | ||||||||||
def NamedTuple(__typename, __fields=_marker, **kwargs): | ||||||||||
"""Typed version of namedtuple. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we're adding a docstring the normal way here, we no longer need to monkey-patch it onto the function afterwards: typing_extensions/src/typing_extensions.py Line 2726 in f4adac5
If you get rid of the docstring monkey-patching, you'll need to ditch this test, as it'll start to fail: typing_extensions/src/test_typing_extensions.py Lines 5098 to 5100 in f9b83a2
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I missed that. I think it's better if we present the same docstring across all versions, so I'll remove the monkey-patching. |
||||||||||
|
||||||||||
Usage:: | ||||||||||
|
||||||||||
class Employee(NamedTuple): | ||||||||||
name: str | ||||||||||
id: int | ||||||||||
|
||||||||||
This is equivalent to:: | ||||||||||
|
||||||||||
Employee = collections.namedtuple('Employee', ['name', 'id']) | ||||||||||
|
||||||||||
The resulting class has an extra __annotations__ attribute, giving a | ||||||||||
dict that maps field names to types. (The field names are also in | ||||||||||
the _fields attribute, which is part of the namedtuple API.) | ||||||||||
An alternative equivalent functional syntax is also accepted:: | ||||||||||
|
||||||||||
Employee = NamedTuple('Employee', [('name', str), ('id', int)]) | ||||||||||
""" | ||||||||||
if __fields is _marker: | ||||||||||
if kwargs: | ||||||||||
deprecated_thing = "Creating NamedTuple classes using keyword arguments" | ||||||||||
deprecation_msg = ( | ||||||||||
"{name} is deprecated and will be disallowed in Python {remove}. " | ||||||||||
"Use the class-based or functional syntax instead." | ||||||||||
) | ||||||||||
else: | ||||||||||
deprecated_thing = "Failing to pass a value for the 'fields' parameter" | ||||||||||
example = f"`{__typename} = NamedTuple({__typename!r}, [])`" | ||||||||||
deprecation_msg = ( | ||||||||||
"{name} is deprecated and will be disallowed in Python {remove}. " | ||||||||||
"To create a NamedTuple class with 0 fields " | ||||||||||
"using the functional syntax, " | ||||||||||
"pass an empty list, e.g. " | ||||||||||
) + example + "." | ||||||||||
elif __fields is None: | ||||||||||
if kwargs: | ||||||||||
raise TypeError( | ||||||||||
"Cannot pass `None` as the 'fields' parameter " | ||||||||||
"and also specify fields using keyword arguments" | ||||||||||
) | ||||||||||
else: | ||||||||||
deprecated_thing = "Passing `None` as the 'fields' parameter" | ||||||||||
example = f"`{__typename} = NamedTuple({__typename!r}, [])`" | ||||||||||
deprecation_msg = ( | ||||||||||
"{name} is deprecated and will be disallowed in Python {remove}. " | ||||||||||
"To create a NamedTuple class with 0 fields " | ||||||||||
"using the functional syntax, " | ||||||||||
"pass an empty list, e.g. " | ||||||||||
) + example + "." | ||||||||||
elif kwargs: | ||||||||||
raise TypeError("Either list of fields or keywords" | ||||||||||
" can be provided to NamedTuple, not both") | ||||||||||
if __fields is _marker or __fields is None: | ||||||||||
warnings.warn( | ||||||||||
deprecation_msg.format(name=deprecated_thing, remove="3.15"), | ||||||||||
DeprecationWarning, | ||||||||||
stacklevel=2, | ||||||||||
) | ||||||||||
__fields = kwargs.items() | ||||||||||
nt = _make_nmtuple(__typename, __fields, module=_caller()) | ||||||||||
nt.__orig_bases__ = (NamedTuple,) | ||||||||||
return nt | ||||||||||
|
||||||||||
NamedTuple.__doc__ = typing.NamedTuple.__doc__ | ||||||||||
_NamedTuple = type.__new__(_NamedTupleMeta, 'NamedTuple', (), {}) | ||||||||||
|
||||||||||
# On 3.8+, alter the signature so that it matches typing.NamedTuple. | ||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tbh this test method feels pretty redundant now; I'd just delete the whole method :p
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It still tests that we have some docstring :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd hope we'd catch it in manual review if somebody proposed deleting the entire docstring in a PR 😄
When I added this test method, my intent was just to test that the monkeypatching in the backport was working correctly. But I guess we can keep it; it doesn't do any harm :)
The PR LGTM!