diff --git a/typing_extensions/README.rst b/typing_extensions/README.rst index 879596e4c..2a3a39e37 100644 --- a/typing_extensions/README.rst +++ b/typing_extensions/README.rst @@ -43,12 +43,14 @@ This module currently contains the following: - ``Literal`` - ``NewType`` - ``NoReturn`` +- ``NotRequired`` - ``overload`` (note that older versions of ``typing`` only let you use ``overload`` in stubs) - ``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 75bdb8ebb..9ae6a242b 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -77,6 +77,9 @@ # Protocols are hard to backport to the original version of typing 3.5.0 HAVE_PROTOCOLS = sys.version_info[:3] != (3, 5, 0) +if TYPING_3_5_3: + from typing_extensions import Required, NotRequired + class BaseTestCase(TestCase): def assertIsSubclass(self, cls, class_or_tuple, msg=None): @@ -229,6 +232,94 @@ def test_no_isinstance(self): issubclass(int, Final) +if TYPING_3_5_3: + 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 184da0ad1..69898c349 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -2844,3 +2844,163 @@ def is_str(val: Union[str, float]): PEP 647 (User-Defined Type Guards). """ __type__ = None + + +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, '{} accepts only single type'.format(self._name)) + 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, '{} accepts only single type'.format(self._name)) + 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 _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, + ) + """) +elif hasattr(typing, '_FinalTypingBase'): + # 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) +else: + # Python 3.5.0 - 3.5.2: Unsupported + pass