mirror of
https://github.com/python/cpython.git
synced 2025-08-31 05:58:33 +00:00
gh-91243: Add typing.Required and NotRequired (PEP 655) (GH-32419)
I talked to @davidfstr and I offered to implement the runtime part of PEP 655 to make sure we can get it in before the feature freeze. We're going to defer the documentation to a separate PR, because it can wait until after the feature freeze. The runtime implementation conveniently already exists in typing-extensions, so I largely copied that. Co-authored-by: David Foster <david@dafoster.net>
This commit is contained in:
parent
474fdbe9e4
commit
ac6c3de03c
4 changed files with 241 additions and 6 deletions
|
@ -6,13 +6,19 @@ look something like this:
|
||||||
|
|
||||||
class Bar(_typed_dict_helper.Foo, total=False):
|
class Bar(_typed_dict_helper.Foo, total=False):
|
||||||
b: int
|
b: int
|
||||||
|
|
||||||
|
In addition, it uses multiple levels of Annotated to test the interaction
|
||||||
|
between the __future__ import, Annotated, and Required.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional, TypedDict
|
from typing import Annotated, Optional, Required, TypedDict
|
||||||
|
|
||||||
OptionalIntType = Optional[int]
|
OptionalIntType = Optional[int]
|
||||||
|
|
||||||
class Foo(TypedDict):
|
class Foo(TypedDict):
|
||||||
a: OptionalIntType
|
a: OptionalIntType
|
||||||
|
|
||||||
|
class VeryAnnotated(TypedDict, total=False):
|
||||||
|
a: Annotated[Annotated[Annotated[Required[int], "a"], "b"], "c"]
|
||||||
|
|
|
@ -23,7 +23,7 @@ from typing import is_typeddict
|
||||||
from typing import reveal_type
|
from typing import reveal_type
|
||||||
from typing import no_type_check, no_type_check_decorator
|
from typing import no_type_check, no_type_check_decorator
|
||||||
from typing import Type
|
from typing import Type
|
||||||
from typing import NamedTuple, TypedDict
|
from typing import NamedTuple, NotRequired, Required, TypedDict
|
||||||
from typing import IO, TextIO, BinaryIO
|
from typing import IO, TextIO, BinaryIO
|
||||||
from typing import Pattern, Match
|
from typing import Pattern, Match
|
||||||
from typing import Annotated, ForwardRef
|
from typing import Annotated, ForwardRef
|
||||||
|
@ -3993,6 +3993,26 @@ class Options(TypedDict, total=False):
|
||||||
log_level: int
|
log_level: int
|
||||||
log_path: str
|
log_path: str
|
||||||
|
|
||||||
|
class TotalMovie(TypedDict):
|
||||||
|
title: str
|
||||||
|
year: NotRequired[int]
|
||||||
|
|
||||||
|
class NontotalMovie(TypedDict, total=False):
|
||||||
|
title: Required[str]
|
||||||
|
year: int
|
||||||
|
|
||||||
|
class AnnotatedMovie(TypedDict):
|
||||||
|
title: Annotated[Required[str], "foobar"]
|
||||||
|
year: NotRequired[Annotated[int, 2000]]
|
||||||
|
|
||||||
|
class DeeplyAnnotatedMovie(TypedDict):
|
||||||
|
title: Annotated[Annotated[Required[str], "foobar"], "another level"]
|
||||||
|
year: NotRequired[Annotated[int, 2000]]
|
||||||
|
|
||||||
|
class WeirdlyQuotedMovie(TypedDict):
|
||||||
|
title: Annotated['Annotated[Required[str], "foobar"]', "another level"]
|
||||||
|
year: NotRequired['Annotated[int, 2000]']
|
||||||
|
|
||||||
class HasForeignBaseClass(mod_generics_cache.A):
|
class HasForeignBaseClass(mod_generics_cache.A):
|
||||||
some_xrepr: 'XRepr'
|
some_xrepr: 'XRepr'
|
||||||
other_a: 'mod_generics_cache.A'
|
other_a: 'mod_generics_cache.A'
|
||||||
|
@ -4280,6 +4300,36 @@ class GetTypeHintTests(BaseTestCase):
|
||||||
):
|
):
|
||||||
get_type_hints(ann_module6)
|
get_type_hints(ann_module6)
|
||||||
|
|
||||||
|
def test_get_type_hints_typeddict(self):
|
||||||
|
self.assertEqual(get_type_hints(TotalMovie), {'title': str, 'year': int})
|
||||||
|
self.assertEqual(get_type_hints(TotalMovie, include_extras=True), {
|
||||||
|
'title': str,
|
||||||
|
'year': NotRequired[int],
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(get_type_hints(AnnotatedMovie), {'title': str, 'year': int})
|
||||||
|
self.assertEqual(get_type_hints(AnnotatedMovie, include_extras=True), {
|
||||||
|
'title': Annotated[Required[str], "foobar"],
|
||||||
|
'year': NotRequired[Annotated[int, 2000]],
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(get_type_hints(DeeplyAnnotatedMovie), {'title': str, 'year': int})
|
||||||
|
self.assertEqual(get_type_hints(DeeplyAnnotatedMovie, include_extras=True), {
|
||||||
|
'title': Annotated[Required[str], "foobar", "another level"],
|
||||||
|
'year': NotRequired[Annotated[int, 2000]],
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(get_type_hints(WeirdlyQuotedMovie), {'title': str, 'year': int})
|
||||||
|
self.assertEqual(get_type_hints(WeirdlyQuotedMovie, include_extras=True), {
|
||||||
|
'title': Annotated[Required[str], "foobar", "another level"],
|
||||||
|
'year': NotRequired[Annotated[int, 2000]],
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(get_type_hints(_typed_dict_helper.VeryAnnotated), {'a': int})
|
||||||
|
self.assertEqual(get_type_hints(_typed_dict_helper.VeryAnnotated, include_extras=True), {
|
||||||
|
'a': Annotated[Required[int], "a", "b", "c"]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class GetUtilitiesTestCase(TestCase):
|
class GetUtilitiesTestCase(TestCase):
|
||||||
def test_get_origin(self):
|
def test_get_origin(self):
|
||||||
|
@ -4305,6 +4355,8 @@ class GetUtilitiesTestCase(TestCase):
|
||||||
self.assertIs(get_origin(list | str), types.UnionType)
|
self.assertIs(get_origin(list | str), types.UnionType)
|
||||||
self.assertIs(get_origin(P.args), P)
|
self.assertIs(get_origin(P.args), P)
|
||||||
self.assertIs(get_origin(P.kwargs), P)
|
self.assertIs(get_origin(P.kwargs), P)
|
||||||
|
self.assertIs(get_origin(Required[int]), Required)
|
||||||
|
self.assertIs(get_origin(NotRequired[int]), NotRequired)
|
||||||
|
|
||||||
def test_get_args(self):
|
def test_get_args(self):
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
@ -4342,6 +4394,8 @@ class GetUtilitiesTestCase(TestCase):
|
||||||
self.assertEqual(get_args(Callable[Concatenate[int, P], int]),
|
self.assertEqual(get_args(Callable[Concatenate[int, P], int]),
|
||||||
(Concatenate[int, P], int))
|
(Concatenate[int, P], int))
|
||||||
self.assertEqual(get_args(list | str), (list, str))
|
self.assertEqual(get_args(list | str), (list, str))
|
||||||
|
self.assertEqual(get_args(Required[int]), (int,))
|
||||||
|
self.assertEqual(get_args(NotRequired[int]), (int,))
|
||||||
|
|
||||||
|
|
||||||
class CollectionsAbcTests(BaseTestCase):
|
class CollectionsAbcTests(BaseTestCase):
|
||||||
|
@ -5299,6 +5353,32 @@ class TypedDictTests(BaseTestCase):
|
||||||
'voice': str,
|
'voice': str,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_required_notrequired_keys(self):
|
||||||
|
self.assertEqual(NontotalMovie.__required_keys__,
|
||||||
|
frozenset({"title"}))
|
||||||
|
self.assertEqual(NontotalMovie.__optional_keys__,
|
||||||
|
frozenset({"year"}))
|
||||||
|
|
||||||
|
self.assertEqual(TotalMovie.__required_keys__,
|
||||||
|
frozenset({"title"}))
|
||||||
|
self.assertEqual(TotalMovie.__optional_keys__,
|
||||||
|
frozenset({"year"}))
|
||||||
|
|
||||||
|
self.assertEqual(_typed_dict_helper.VeryAnnotated.__required_keys__,
|
||||||
|
frozenset())
|
||||||
|
self.assertEqual(_typed_dict_helper.VeryAnnotated.__optional_keys__,
|
||||||
|
frozenset({"a"}))
|
||||||
|
|
||||||
|
self.assertEqual(AnnotatedMovie.__required_keys__,
|
||||||
|
frozenset({"title"}))
|
||||||
|
self.assertEqual(AnnotatedMovie.__optional_keys__,
|
||||||
|
frozenset({"year"}))
|
||||||
|
|
||||||
|
self.assertEqual(WeirdlyQuotedMovie.__required_keys__,
|
||||||
|
frozenset({"title"}))
|
||||||
|
self.assertEqual(WeirdlyQuotedMovie.__optional_keys__,
|
||||||
|
frozenset({"year"}))
|
||||||
|
|
||||||
def test_multiple_inheritance(self):
|
def test_multiple_inheritance(self):
|
||||||
class One(TypedDict):
|
class One(TypedDict):
|
||||||
one: int
|
one: int
|
||||||
|
@ -5399,6 +5479,98 @@ class TypedDictTests(BaseTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RequiredTests(BaseTestCase):
|
||||||
|
|
||||||
|
def test_basics(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
Required[NotRequired]
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
Required[int, str]
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
Required[int][str]
|
||||||
|
|
||||||
|
def test_repr(self):
|
||||||
|
self.assertEqual(repr(Required), 'typing.Required')
|
||||||
|
cv = Required[int]
|
||||||
|
self.assertEqual(repr(cv), 'typing.Required[int]')
|
||||||
|
cv = Required[Employee]
|
||||||
|
self.assertEqual(repr(cv), f'typing.Required[{__name__}.Employee]')
|
||||||
|
|
||||||
|
def test_cannot_subclass(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
class C(type(Required)):
|
||||||
|
pass
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
class C(type(Required[int])):
|
||||||
|
pass
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
class C(Required):
|
||||||
|
pass
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
class C(Required[int]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_cannot_init(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
Required()
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
type(Required)()
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
type(Required[Optional[int]])()
|
||||||
|
|
||||||
|
def test_no_isinstance(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
isinstance(1, Required[int])
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
issubclass(int, Required)
|
||||||
|
|
||||||
|
|
||||||
|
class NotRequiredTests(BaseTestCase):
|
||||||
|
|
||||||
|
def test_basics(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
NotRequired[Required]
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
NotRequired[int, str]
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
NotRequired[int][str]
|
||||||
|
|
||||||
|
def test_repr(self):
|
||||||
|
self.assertEqual(repr(NotRequired), 'typing.NotRequired')
|
||||||
|
cv = NotRequired[int]
|
||||||
|
self.assertEqual(repr(cv), 'typing.NotRequired[int]')
|
||||||
|
cv = NotRequired[Employee]
|
||||||
|
self.assertEqual(repr(cv), f'typing.NotRequired[{__name__}.Employee]')
|
||||||
|
|
||||||
|
def test_cannot_subclass(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
class C(type(NotRequired)):
|
||||||
|
pass
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
class C(type(NotRequired[int])):
|
||||||
|
pass
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
class C(NotRequired):
|
||||||
|
pass
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
class C(NotRequired[int]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_cannot_init(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
NotRequired()
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
type(NotRequired)()
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
type(NotRequired[Optional[int]])()
|
||||||
|
|
||||||
|
def test_no_isinstance(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
isinstance(1, NotRequired[int])
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
issubclass(int, NotRequired)
|
||||||
|
|
||||||
|
|
||||||
class IOTests(BaseTestCase):
|
class IOTests(BaseTestCase):
|
||||||
|
|
||||||
def test_io(self):
|
def test_io(self):
|
||||||
|
|
|
@ -132,9 +132,11 @@ __all__ = [
|
||||||
'no_type_check',
|
'no_type_check',
|
||||||
'no_type_check_decorator',
|
'no_type_check_decorator',
|
||||||
'NoReturn',
|
'NoReturn',
|
||||||
|
'NotRequired',
|
||||||
'overload',
|
'overload',
|
||||||
'ParamSpecArgs',
|
'ParamSpecArgs',
|
||||||
'ParamSpecKwargs',
|
'ParamSpecKwargs',
|
||||||
|
'Required',
|
||||||
'reveal_type',
|
'reveal_type',
|
||||||
'runtime_checkable',
|
'runtime_checkable',
|
||||||
'Self',
|
'Self',
|
||||||
|
@ -2262,6 +2264,8 @@ def _strip_annotations(t):
|
||||||
"""
|
"""
|
||||||
if isinstance(t, _AnnotatedAlias):
|
if isinstance(t, _AnnotatedAlias):
|
||||||
return _strip_annotations(t.__origin__)
|
return _strip_annotations(t.__origin__)
|
||||||
|
if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired):
|
||||||
|
return _strip_annotations(t.__args__[0])
|
||||||
if isinstance(t, _GenericAlias):
|
if isinstance(t, _GenericAlias):
|
||||||
stripped_args = tuple(_strip_annotations(a) for a in t.__args__)
|
stripped_args = tuple(_strip_annotations(a) for a in t.__args__)
|
||||||
if stripped_args == t.__args__:
|
if stripped_args == t.__args__:
|
||||||
|
@ -2786,10 +2790,22 @@ class _TypedDictMeta(type):
|
||||||
optional_keys.update(base.__dict__.get('__optional_keys__', ()))
|
optional_keys.update(base.__dict__.get('__optional_keys__', ()))
|
||||||
|
|
||||||
annotations.update(own_annotations)
|
annotations.update(own_annotations)
|
||||||
if total:
|
for annotation_key, annotation_type in own_annotations.items():
|
||||||
required_keys.update(own_annotation_keys)
|
annotation_origin = get_origin(annotation_type)
|
||||||
else:
|
if annotation_origin is Annotated:
|
||||||
optional_keys.update(own_annotation_keys)
|
annotation_args = get_args(annotation_type)
|
||||||
|
if annotation_args:
|
||||||
|
annotation_type = annotation_args[0]
|
||||||
|
annotation_origin = get_origin(annotation_type)
|
||||||
|
|
||||||
|
if annotation_origin is Required:
|
||||||
|
required_keys.add(annotation_key)
|
||||||
|
elif annotation_origin is NotRequired:
|
||||||
|
optional_keys.add(annotation_key)
|
||||||
|
elif total:
|
||||||
|
required_keys.add(annotation_key)
|
||||||
|
else:
|
||||||
|
optional_keys.add(annotation_key)
|
||||||
|
|
||||||
tp_dict.__annotations__ = annotations
|
tp_dict.__annotations__ = annotations
|
||||||
tp_dict.__required_keys__ = frozenset(required_keys)
|
tp_dict.__required_keys__ = frozenset(required_keys)
|
||||||
|
@ -2874,6 +2890,45 @@ _TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {})
|
||||||
TypedDict.__mro_entries__ = lambda bases: (_TypedDict,)
|
TypedDict.__mro_entries__ = lambda bases: (_TypedDict,)
|
||||||
|
|
||||||
|
|
||||||
|
@_SpecialForm
|
||||||
|
def Required(self, parameters):
|
||||||
|
"""A special typing construct to mark a key of a total=False TypedDict
|
||||||
|
as required. For example:
|
||||||
|
|
||||||
|
class Movie(TypedDict, total=False):
|
||||||
|
title: Required[str]
|
||||||
|
year: int
|
||||||
|
|
||||||
|
m = Movie(
|
||||||
|
title='The Matrix', # typechecker error if key is omitted
|
||||||
|
year=1999,
|
||||||
|
)
|
||||||
|
|
||||||
|
There is no runtime checking that a required key is actually provided
|
||||||
|
when instantiating a related TypedDict.
|
||||||
|
"""
|
||||||
|
item = _type_check(parameters, f'{self._name} accepts only a single type.')
|
||||||
|
return _GenericAlias(self, (item,))
|
||||||
|
|
||||||
|
|
||||||
|
@_SpecialForm
|
||||||
|
def NotRequired(self, parameters):
|
||||||
|
"""A special typing construct to mark a key of a TypedDict as
|
||||||
|
potentially missing. For example:
|
||||||
|
|
||||||
|
class Movie(TypedDict):
|
||||||
|
title: str
|
||||||
|
year: NotRequired[int]
|
||||||
|
|
||||||
|
m = Movie(
|
||||||
|
title='The Matrix', # typechecker error if key is omitted
|
||||||
|
year=1999,
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
item = _type_check(parameters, f'{self._name} accepts only a single type.')
|
||||||
|
return _GenericAlias(self, (item,))
|
||||||
|
|
||||||
|
|
||||||
class NewType:
|
class NewType:
|
||||||
"""NewType creates simple unique types with almost zero
|
"""NewType creates simple unique types with almost zero
|
||||||
runtime overhead. NewType(name, tp) is considered a subtype of tp
|
runtime overhead. NewType(name, tp) is considered a subtype of tp
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Implement ``typing.Required`` and ``typing.NotRequired`` (:pep:`655`). Patch
|
||||||
|
by Jelle Zijlstra.
|
Loading…
Add table
Add a link
Reference in a new issue