Skip to content

Commit 5c98e79

Browse files
Add PEP 655 Required and NotRequired to typing_extensions (#937)
Co-authored-by: David Foster <david@dafoster.net>
1 parent 1118e9d commit 5c98e79

File tree

4 files changed

+259
-9
lines changed

4 files changed

+259
-9
lines changed

typing_extensions/CHANGELOG

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
# Changes in version 4.0.0
22

3-
Starting with version 4.0.0, typing_extensions uses Semantic Versioning.
4-
See the README for more information.
5-
6-
Dropped support for Python versions 3.5 and older.
7-
8-
Simplified backports for Python 3.6.0 and newer.
9-
Patch by Adam Turner (@AA-Turner).
3+
- Starting with version 4.0.0, typing_extensions uses Semantic Versioning.
4+
See the README for more information.
5+
- Dropped support for Python versions 3.5 and older.
6+
- Simplified backports for Python 3.6.0 and newer. Patch by Adam Turner (@AA-Turner).
107

118
## Added in version 4.0.0
129

13-
- Runtime support for PEP 673 and `typing_extensions.Self`.
10+
- Runtime support for PEP 673 and `typing_extensions.Self`. Patch by
11+
James Hilton-Balfe (@Gobot1234).
12+
- Runtime support for PEP 655 and `typing_extensions.Required` and `NotRequired`.
13+
Patch by David Foster (@davidfstr).
1414

1515
## Removed in version 4.0.0
1616

typing_extensions/README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@ This module currently contains the following:
5050
- ``Literal``
5151
- ``NewType``
5252
- ``NoReturn``
53+
- ``NotRequired``
5354
- ``overload``
5455
- ``OrderedDict``
5556
- ``ParamSpec``
5657
- ``ParamSpecArgs``
5758
- ``ParamSpecKwargs``
5859
- ``Protocol``
60+
- ``Required``
5961
- ``runtime_checkable``
6062
- ``Text``
6163
- ``Type``

typing_extensions/src_py3/test_typing_extensions.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import typing_extensions
1919
from typing_extensions import NoReturn, ClassVar, Final, IntVar, Literal, Type, NewType, TypedDict, Self
2020
from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs, TypeGuard
21-
from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager
21+
from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired
2222
from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, overload
2323
try:
2424
from typing_extensions import get_type_hints
@@ -193,6 +193,94 @@ def test_no_isinstance(self):
193193
issubclass(int, Final)
194194

195195

196+
class RequiredTests(BaseTestCase):
197+
198+
def test_basics(self):
199+
with self.assertRaises(TypeError):
200+
Required[1]
201+
with self.assertRaises(TypeError):
202+
Required[int, str]
203+
with self.assertRaises(TypeError):
204+
Required[int][str]
205+
206+
def test_repr(self):
207+
if hasattr(typing, 'Required'):
208+
mod_name = 'typing'
209+
else:
210+
mod_name = 'typing_extensions'
211+
self.assertEqual(repr(Required), mod_name + '.Required')
212+
cv = Required[int]
213+
self.assertEqual(repr(cv), mod_name + '.Required[int]')
214+
cv = Required[Employee]
215+
self.assertEqual(repr(cv), mod_name + '.Required[%s.Employee]' % __name__)
216+
217+
def test_cannot_subclass(self):
218+
with self.assertRaises(TypeError):
219+
class C(type(Required)):
220+
pass
221+
with self.assertRaises(TypeError):
222+
class C(type(Required[int])):
223+
pass
224+
225+
def test_cannot_init(self):
226+
with self.assertRaises(TypeError):
227+
Required()
228+
with self.assertRaises(TypeError):
229+
type(Required)()
230+
with self.assertRaises(TypeError):
231+
type(Required[Optional[int]])()
232+
233+
def test_no_isinstance(self):
234+
with self.assertRaises(TypeError):
235+
isinstance(1, Required[int])
236+
with self.assertRaises(TypeError):
237+
issubclass(int, Required)
238+
239+
240+
class NotRequiredTests(BaseTestCase):
241+
242+
def test_basics(self):
243+
with self.assertRaises(TypeError):
244+
NotRequired[1]
245+
with self.assertRaises(TypeError):
246+
NotRequired[int, str]
247+
with self.assertRaises(TypeError):
248+
NotRequired[int][str]
249+
250+
def test_repr(self):
251+
if hasattr(typing, 'NotRequired'):
252+
mod_name = 'typing'
253+
else:
254+
mod_name = 'typing_extensions'
255+
self.assertEqual(repr(NotRequired), mod_name + '.NotRequired')
256+
cv = NotRequired[int]
257+
self.assertEqual(repr(cv), mod_name + '.NotRequired[int]')
258+
cv = NotRequired[Employee]
259+
self.assertEqual(repr(cv), mod_name + '.NotRequired[%s.Employee]' % __name__)
260+
261+
def test_cannot_subclass(self):
262+
with self.assertRaises(TypeError):
263+
class C(type(NotRequired)):
264+
pass
265+
with self.assertRaises(TypeError):
266+
class C(type(NotRequired[int])):
267+
pass
268+
269+
def test_cannot_init(self):
270+
with self.assertRaises(TypeError):
271+
NotRequired()
272+
with self.assertRaises(TypeError):
273+
type(NotRequired)()
274+
with self.assertRaises(TypeError):
275+
type(NotRequired[Optional[int]])()
276+
277+
def test_no_isinstance(self):
278+
with self.assertRaises(TypeError):
279+
isinstance(1, NotRequired[int])
280+
with self.assertRaises(TypeError):
281+
issubclass(int, NotRequired)
282+
283+
196284
class IntVarTests(BaseTestCase):
197285
def test_valid(self):
198286
T_ints = IntVar("T_ints") # noqa

typing_extensions/src_py3/typing_extensions.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2118,3 +2118,163 @@ def __subclasscheck__(self, cls):
21182118
raise TypeError(f"{self} cannot be used with issubclass().")
21192119

21202120
Self = _Self(_root=True)
2121+
2122+
2123+
if hasattr(typing, 'Required'):
2124+
Required = typing.Required
2125+
NotRequired = typing.NotRequired
2126+
elif sys.version_info[:2] >= (3, 9):
2127+
class _ExtensionsSpecialForm(typing._SpecialForm, _root=True):
2128+
def __repr__(self):
2129+
return 'typing_extensions.' + self._name
2130+
2131+
@_ExtensionsSpecialForm
2132+
def Required(self, parameters):
2133+
"""A special typing construct to mark a key of a total=False TypedDict
2134+
as required. For example:
2135+
2136+
class Movie(TypedDict, total=False):
2137+
title: Required[str]
2138+
year: int
2139+
2140+
m = Movie(
2141+
title='The Matrix', # typechecker error if key is omitted
2142+
year=1999,
2143+
)
2144+
2145+
There is no runtime checking that a required key is actually provided
2146+
when instantiating a related TypedDict.
2147+
"""
2148+
item = typing._type_check(parameters, f'{self._name} accepts only single type')
2149+
return typing._GenericAlias(self, (item,))
2150+
2151+
@_ExtensionsSpecialForm
2152+
def NotRequired(self, parameters):
2153+
"""A special typing construct to mark a key of a TypedDict as
2154+
potentially missing. For example:
2155+
2156+
class Movie(TypedDict):
2157+
title: str
2158+
year: NotRequired[int]
2159+
2160+
m = Movie(
2161+
title='The Matrix', # typechecker error if key is omitted
2162+
year=1999,
2163+
)
2164+
"""
2165+
item = typing._type_check(parameters, f'{self._name} accepts only single type')
2166+
return typing._GenericAlias(self, (item,))
2167+
2168+
elif sys.version_info[:2] >= (3, 7):
2169+
class _RequiredForm(typing._SpecialForm, _root=True):
2170+
def __repr__(self):
2171+
return 'typing_extensions.' + self._name
2172+
2173+
def __getitem__(self, parameters):
2174+
item = typing._type_check(parameters,
2175+
'{} accepts only single type'.format(self._name))
2176+
return typing._GenericAlias(self, (item,))
2177+
2178+
Required = _RequiredForm(
2179+
'Required',
2180+
doc="""A special typing construct to mark a key of a total=False TypedDict
2181+
as required. For example:
2182+
2183+
class Movie(TypedDict, total=False):
2184+
title: Required[str]
2185+
year: int
2186+
2187+
m = Movie(
2188+
title='The Matrix', # typechecker error if key is omitted
2189+
year=1999,
2190+
)
2191+
2192+
There is no runtime checking that a required key is actually provided
2193+
when instantiating a related TypedDict.
2194+
""")
2195+
NotRequired = _RequiredForm(
2196+
'NotRequired',
2197+
doc="""A special typing construct to mark a key of a TypedDict as
2198+
potentially missing. For example:
2199+
2200+
class Movie(TypedDict):
2201+
title: str
2202+
year: NotRequired[int]
2203+
2204+
m = Movie(
2205+
title='The Matrix', # typechecker error if key is omitted
2206+
year=1999,
2207+
)
2208+
""")
2209+
else:
2210+
# NOTE: Modeled after _Final's implementation when _FinalTypingBase available
2211+
class _MaybeRequired(typing._FinalTypingBase, _root=True):
2212+
__slots__ = ('__type__',)
2213+
2214+
def __init__(self, tp=None, **kwds):
2215+
self.__type__ = tp
2216+
2217+
def __getitem__(self, item):
2218+
cls = type(self)
2219+
if self.__type__ is None:
2220+
return cls(typing._type_check(item,
2221+
'{} accepts only single type.'.format(cls.__name__[1:])),
2222+
_root=True)
2223+
raise TypeError('{} cannot be further subscripted'
2224+
.format(cls.__name__[1:]))
2225+
2226+
def _eval_type(self, globalns, localns):
2227+
new_tp = typing._eval_type(self.__type__, globalns, localns)
2228+
if new_tp == self.__type__:
2229+
return self
2230+
return type(self)(new_tp, _root=True)
2231+
2232+
def __repr__(self):
2233+
r = super().__repr__()
2234+
if self.__type__ is not None:
2235+
r += '[{}]'.format(typing._type_repr(self.__type__))
2236+
return r
2237+
2238+
def __hash__(self):
2239+
return hash((type(self).__name__, self.__type__))
2240+
2241+
def __eq__(self, other):
2242+
if not isinstance(other, _Final):
2243+
return NotImplemented
2244+
if self.__type__ is not None:
2245+
return self.__type__ == other.__type__
2246+
return self is other
2247+
2248+
class _Required(_MaybeRequired, _root=True):
2249+
"""A special typing construct to mark a key of a total=False TypedDict
2250+
as required. For example:
2251+
2252+
class Movie(TypedDict, total=False):
2253+
title: Required[str]
2254+
year: int
2255+
2256+
m = Movie(
2257+
title='The Matrix', # typechecker error if key is omitted
2258+
year=1999,
2259+
)
2260+
2261+
There is no runtime checking that a required key is actually provided
2262+
when instantiating a related TypedDict.
2263+
"""
2264+
2265+
class _NotRequired(_MaybeRequired, _root=True):
2266+
"""A special typing construct to mark a key of a TypedDict as
2267+
potentially missing. For example:
2268+
2269+
class Movie(TypedDict):
2270+
title: str
2271+
year: NotRequired[int]
2272+
2273+
m = Movie(
2274+
title='The Matrix', # typechecker error if key is omitted
2275+
year=1999,
2276+
)
2277+
"""
2278+
2279+
Required = _Required(_root=True)
2280+
NotRequired = _NotRequired(_root=True)

0 commit comments

Comments
 (0)