From 5c98e79c676b51688b5e27ccac2010d23cb235ad Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 14 Nov 2021 06:22:30 -0800 Subject: [PATCH] Add PEP 655 Required and NotRequired to typing_extensions (#937) Co-authored-by: David Foster --- typing_extensions/CHANGELOG | 16 +- typing_extensions/README.rst | 2 + .../src_py3/test_typing_extensions.py | 90 +++++++++- .../src_py3/typing_extensions.py | 160 ++++++++++++++++++ 4 files changed, 259 insertions(+), 9 deletions(-) diff --git a/typing_extensions/CHANGELOG b/typing_extensions/CHANGELOG index 4c3a51d6c..897d9ad66 100644 --- a/typing_extensions/CHANGELOG +++ b/typing_extensions/CHANGELOG @@ -1,16 +1,16 @@ # Changes in version 4.0.0 -Starting with version 4.0.0, typing_extensions uses Semantic Versioning. -See the README for more information. - -Dropped support for Python versions 3.5 and older. - -Simplified backports for Python 3.6.0 and newer. -Patch by Adam Turner (@AA-Turner). +- Starting with version 4.0.0, typing_extensions uses Semantic Versioning. + See the README for more information. +- Dropped support for Python versions 3.5 and older. +- Simplified backports for Python 3.6.0 and newer. Patch by Adam Turner (@AA-Turner). ## Added in version 4.0.0 -- Runtime support for PEP 673 and `typing_extensions.Self`. +- Runtime support for PEP 673 and `typing_extensions.Self`. Patch by + James Hilton-Balfe (@Gobot1234). +- Runtime support for PEP 655 and `typing_extensions.Required` and `NotRequired`. + Patch by David Foster (@davidfstr). ## Removed in version 4.0.0 diff --git a/typing_extensions/README.rst b/typing_extensions/README.rst index c89fc0425..769535e6f 100644 --- a/typing_extensions/README.rst +++ b/typing_extensions/README.rst @@ -50,12 +50,14 @@ This module currently contains the following: - ``Literal`` - ``NewType`` - ``NoReturn`` +- ``NotRequired`` - ``overload`` - ``OrderedDict`` - ``ParamSpec`` - ``ParamSpecArgs`` - ``ParamSpecKwargs`` - ``Protocol`` +- ``Required`` - ``runtime_checkable`` - ``Text`` - ``Type`` diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index 841d08cfc..2fc5b3f2a 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -18,7 +18,7 @@ import typing_extensions from typing_extensions import NoReturn, ClassVar, Final, IntVar, Literal, Type, NewType, TypedDict, Self from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs, TypeGuard -from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager +from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, overload try: from typing_extensions import get_type_hints @@ -193,6 +193,94 @@ def test_no_isinstance(self): issubclass(int, Final) +class RequiredTests(BaseTestCase): + + def test_basics(self): + with self.assertRaises(TypeError): + Required[1] + with self.assertRaises(TypeError): + Required[int, str] + with self.assertRaises(TypeError): + Required[int][str] + + def test_repr(self): + if hasattr(typing, 'Required'): + mod_name = 'typing' + else: + mod_name = 'typing_extensions' + self.assertEqual(repr(Required), mod_name + '.Required') + cv = Required[int] + self.assertEqual(repr(cv), mod_name + '.Required[int]') + cv = Required[Employee] + self.assertEqual(repr(cv), mod_name + '.Required[%s.Employee]' % __name__) + + def test_cannot_subclass(self): + with self.assertRaises(TypeError): + class C(type(Required)): + pass + with self.assertRaises(TypeError): + class C(type(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[1] + with self.assertRaises(TypeError): + NotRequired[int, str] + with self.assertRaises(TypeError): + NotRequired[int][str] + + def test_repr(self): + if hasattr(typing, 'NotRequired'): + mod_name = 'typing' + else: + mod_name = 'typing_extensions' + self.assertEqual(repr(NotRequired), mod_name + '.NotRequired') + cv = NotRequired[int] + self.assertEqual(repr(cv), mod_name + '.NotRequired[int]') + cv = NotRequired[Employee] + self.assertEqual(repr(cv), mod_name + '.NotRequired[%s.Employee]' % __name__) + + def test_cannot_subclass(self): + with self.assertRaises(TypeError): + class C(type(NotRequired)): + pass + with self.assertRaises(TypeError): + class C(type(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 IntVarTests(BaseTestCase): def test_valid(self): T_ints = IntVar("T_ints") # noqa diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 3e5b488c2..d50f710e8 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -2118,3 +2118,163 @@ def __subclasscheck__(self, cls): raise TypeError(f"{self} cannot be used with issubclass().") Self = _Self(_root=True) + + +if hasattr(typing, 'Required'): + Required = typing.Required + NotRequired = typing.NotRequired +elif sys.version_info[:2] >= (3, 9): + class _ExtensionsSpecialForm(typing._SpecialForm, _root=True): + def __repr__(self): + return 'typing_extensions.' + self._name + + @_ExtensionsSpecialForm + 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 = typing._type_check(parameters, f'{self._name} accepts only single type') + return typing._GenericAlias(self, (item,)) + + @_ExtensionsSpecialForm + 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 = typing._type_check(parameters, f'{self._name} accepts only single type') + return typing._GenericAlias(self, (item,)) + +elif sys.version_info[:2] >= (3, 7): + class _RequiredForm(typing._SpecialForm, _root=True): + def __repr__(self): + return 'typing_extensions.' + self._name + + def __getitem__(self, parameters): + item = typing._type_check(parameters, + '{} accepts only single type'.format(self._name)) + return typing._GenericAlias(self, (item,)) + + Required = _RequiredForm( + 'Required', + doc="""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. + """) + NotRequired = _RequiredForm( + 'NotRequired', + doc="""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, + ) + """) +else: + # NOTE: Modeled after _Final's implementation when _FinalTypingBase available + class _MaybeRequired(typing._FinalTypingBase, _root=True): + __slots__ = ('__type__',) + + def __init__(self, tp=None, **kwds): + self.__type__ = tp + + def __getitem__(self, item): + cls = type(self) + if self.__type__ is None: + return cls(typing._type_check(item, + '{} accepts only single type.'.format(cls.__name__[1:])), + _root=True) + raise TypeError('{} cannot be further subscripted' + .format(cls.__name__[1:])) + + def _eval_type(self, globalns, localns): + new_tp = typing._eval_type(self.__type__, globalns, localns) + if new_tp == self.__type__: + return self + return type(self)(new_tp, _root=True) + + def __repr__(self): + r = super().__repr__() + if self.__type__ is not None: + r += '[{}]'.format(typing._type_repr(self.__type__)) + return r + + def __hash__(self): + return hash((type(self).__name__, self.__type__)) + + def __eq__(self, other): + if not isinstance(other, _Final): + return NotImplemented + if self.__type__ is not None: + return self.__type__ == other.__type__ + return self is other + + class _Required(_MaybeRequired, _root=True): + """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. + """ + + class _NotRequired(_MaybeRequired, _root=True): + """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, + ) + """ + + Required = _Required(_root=True) + NotRequired = _NotRequired(_root=True)