Skip to content

Commit 479dae1

Browse files
Add support for sentinels (PEP 661) (#594)
Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent f74a56a commit 479dae1

File tree

4 files changed

+107
-0
lines changed

4 files changed

+107
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ New features:
1414
Patch by [Victorien Plot](https://github.com/Viicos).
1515
- Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by
1616
Sebastian Rittau.
17+
- Add support for sentinels ([PEP 661](https://peps.python.org/pep-0661/)). Patch by
18+
[Victorien Plot](https://github.com/Viicos).
1719

1820
# Release 4.13.2 (April 10, 2025)
1921

doc/index.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,34 @@ Capsule objects
10271027
.. versionadded:: 4.12.0
10281028

10291029

1030+
Sentinel objects
1031+
~~~~~~~~~~~~~~~~
1032+
1033+
.. class:: Sentinel(name, repr=None)
1034+
1035+
A type used to define sentinel values. The *name* argument should be the
1036+
name of the variable to which the return value shall be assigned.
1037+
1038+
If *repr* is provided, it will be used for the :meth:`~object.__repr__`
1039+
of the sentinel object. If not provided, ``"<name>"`` will be used.
1040+
1041+
Example::
1042+
1043+
>>> from typing_extensions import Sentinel, assert_type
1044+
>>> MISSING = Sentinel('MISSING')
1045+
>>> def func(arg: int | MISSING = MISSING) -> None:
1046+
... if arg is MISSING:
1047+
... assert_type(arg, MISSING)
1048+
... else:
1049+
... assert_type(arg, int)
1050+
...
1051+
>>> func(MISSING)
1052+
1053+
.. versionadded:: 4.14.0
1054+
1055+
See :pep:`661`
1056+
1057+
10301058
Pure aliases
10311059
~~~~~~~~~~~~
10321060

src/test_typing_extensions.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
ReadOnly,
6666
Required,
6767
Self,
68+
Sentinel,
6869
Set,
6970
Tuple,
7071
Type,
@@ -9096,5 +9097,44 @@ def test_invalid_special_forms(self):
90969097
self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)), ClassVar)
90979098

90989099

9100+
class TestSentinels(BaseTestCase):
9101+
def test_sentinel_no_repr(self):
9102+
sentinel_no_repr = Sentinel('sentinel_no_repr')
9103+
9104+
self.assertEqual(sentinel_no_repr._name, 'sentinel_no_repr')
9105+
self.assertEqual(repr(sentinel_no_repr), '<sentinel_no_repr>')
9106+
9107+
def test_sentinel_explicit_repr(self):
9108+
sentinel_explicit_repr = Sentinel('sentinel_explicit_repr', repr='explicit_repr')
9109+
9110+
self.assertEqual(repr(sentinel_explicit_repr), 'explicit_repr')
9111+
9112+
@skipIf(sys.version_info < (3, 10), reason='New unions not available in 3.9')
9113+
def test_sentinel_type_expression_union(self):
9114+
sentinel = Sentinel('sentinel')
9115+
9116+
def func1(a: int | sentinel = sentinel): pass
9117+
def func2(a: sentinel | int = sentinel): pass
9118+
9119+
self.assertEqual(func1.__annotations__['a'], Union[int, sentinel])
9120+
self.assertEqual(func2.__annotations__['a'], Union[sentinel, int])
9121+
9122+
def test_sentinel_not_callable(self):
9123+
sentinel = Sentinel('sentinel')
9124+
with self.assertRaisesRegex(
9125+
TypeError,
9126+
"'Sentinel' object is not callable"
9127+
):
9128+
sentinel()
9129+
9130+
def test_sentinel_not_picklable(self):
9131+
sentinel = Sentinel('sentinel')
9132+
with self.assertRaisesRegex(
9133+
TypeError,
9134+
"Cannot pickle 'Sentinel' object"
9135+
):
9136+
pickle.dumps(sentinel)
9137+
9138+
90999139
if __name__ == '__main__':
91009140
main()

src/typing_extensions.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
'overload',
9090
'override',
9191
'Protocol',
92+
'Sentinel',
9293
'reveal_type',
9394
'runtime',
9495
'runtime_checkable',
@@ -4210,6 +4211,42 @@ def evaluate_forward_ref(
42104211
)
42114212

42124213

4214+
class Sentinel:
4215+
"""Create a unique sentinel object.
4216+
4217+
*name* should be the name of the variable to which the return value shall be assigned.
4218+
4219+
*repr*, if supplied, will be used for the repr of the sentinel object.
4220+
If not provided, "<name>" will be used.
4221+
"""
4222+
4223+
def __init__(
4224+
self,
4225+
name: str,
4226+
repr: typing.Optional[str] = None,
4227+
):
4228+
self._name = name
4229+
self._repr = repr if repr is not None else f'<{name}>'
4230+
4231+
def __repr__(self):
4232+
return self._repr
4233+
4234+
if sys.version_info < (3, 11):
4235+
# The presence of this method convinces typing._type_check
4236+
# that Sentinels are types.
4237+
def __call__(self, *args, **kwargs):
4238+
raise TypeError(f"{type(self).__name__!r} object is not callable")
4239+
4240+
def __or__(self, other):
4241+
return typing.Union[self, other]
4242+
4243+
def __ror__(self, other):
4244+
return typing.Union[other, self]
4245+
4246+
def __getstate__(self):
4247+
raise TypeError(f"Cannot pickle {type(self).__name__!r} object")
4248+
4249+
42134250
# Aliases for items that are in typing in all supported versions.
42144251
# We use hasattr() checks so this library will continue to import on
42154252
# future versions of Python that may remove these names.

0 commit comments

Comments
 (0)