Skip to content
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

Add PEP 655 Required and NotRequired to typing_extensions #807

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion typing_extensions/README.rst
Expand Up @@ -57,10 +57,12 @@ Python 3.4+ only:
-----------------

- ``ChainMap``
- ``ParamSpec``
- ``Concatenate``
- ``NotRequired`` (except on Python 3.5.0-3.5.2)
- ``ParamSpec``
- ``ParamSpecArgs``
- ``ParamSpecKwargs``
- ``Required`` (except on Python 3.5.0-3.5.2)
- ``TypeGuard``

Python 3.5+ only:
Expand Down
91 changes: 91 additions & 0 deletions typing_extensions/src_py3/test_typing_extensions.py
Expand Up @@ -76,6 +76,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):
Expand Down Expand Up @@ -228,6 +231,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
Expand Down
160 changes: 160 additions & 0 deletions typing_extensions/src_py3/typing_extensions.py
Expand Up @@ -2803,3 +2803,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
davidfstr marked this conversation as resolved.
Show resolved Hide resolved
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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems incorrect; it will make Required[str] == NotRequired[str] return True.

Also, could you add tests for __eq__?

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