Skip to content

Commit

Permalink
Add PEP 655 Required and NotRequired to typing_extensions (#937)
Browse files Browse the repository at this point in the history
Co-authored-by: David Foster <[email protected]>
  • Loading branch information
JelleZijlstra and davidfstr authored Nov 14, 2021
1 parent 1118e9d commit 5c98e79
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 9 deletions.
16 changes: 8 additions & 8 deletions typing_extensions/CHANGELOG
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 2 additions & 0 deletions typing_extensions/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``
Expand Down
90 changes: 89 additions & 1 deletion typing_extensions/src_py3/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
160 changes: 160 additions & 0 deletions typing_extensions/src_py3/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit 5c98e79

Please sign in to comment.