Skip to content

Commit ac22509

Browse files
committed
Add support for sentinels (PEP 661)
1 parent 2523523 commit ac22509

File tree

3 files changed

+83
-0
lines changed

3 files changed

+83
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ New features:
99
- Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by
1010
Sebastian Rittau.
1111
- Fix tests for Python 3.14. Patch by Jelle Zijlstra.
12+
- Add support for sentinels ([PEP 661](https://peps.python.org/pep-0661/)).
1213

1314
# Release 4.13.2 (April 10, 2025)
1415

src/test_typing_extensions.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
ReadOnly,
6767
Required,
6868
Self,
69+
Sentinel,
6970
Set,
7071
Tuple,
7172
Type,
@@ -9088,5 +9089,40 @@ def test_invalid_special_forms(self):
90889089
self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)), ClassVar)
90899090

90909091

9092+
class TestSentinels(BaseTestCase):
9093+
def test_sentinel_no_repr(self):
9094+
sentinel_no_repr = Sentinel('sentinel_no_repr')
9095+
9096+
self.assertEqual(sentinel_no_repr._name, 'sentinel_no_repr')
9097+
self.assertEqual(repr(sentinel_no_repr), '<sentinel_no_repr>')
9098+
9099+
sentinel_no_repr_dots = Sentinel('Test.sentinel_no_repr')
9100+
9101+
self.assertEqual(sentinel_no_repr_dots._name, 'Test.sentinel_no_repr')
9102+
self.assertEqual(repr(sentinel_no_repr), '<sentinel_no_repr>')
9103+
9104+
def test_sentinel_explicit_repr(self):
9105+
sentinel_explicit_repr = Sentinel('sentinel_explicit_repr', repr='explicit_repr')
9106+
9107+
self.assertEqual(repr(sentinel_explicit_repr), 'explicit_repr')
9108+
9109+
def test_sentinel_type_expression_union(self):
9110+
sentinel = Sentinel('sentinel')
9111+
9112+
def func1(a: int | sentinel = sentinel): pass
9113+
def func2(a: sentinel | int = sentinel): pass
9114+
9115+
self.assertEqual(func1.__annotations__['a'], Union[int, sentinel])
9116+
self.assertEqual(func2.__annotations__['a'], Union[sentinel, int])
9117+
9118+
def test_sentinel_not_callable(self):
9119+
sentinel = Sentinel('sentinel')
9120+
with self.assertRaisesRegex(
9121+
TypeError,
9122+
"'Sentinel' object is not callable"
9123+
):
9124+
sentinel()
9125+
9126+
90919127
if __name__ == '__main__':
90929128
main()

src/typing_extensions.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# pyright: ignore
12
import abc
23
import builtins
34
import collections
@@ -89,6 +90,7 @@
8990
'overload',
9091
'override',
9192
'Protocol',
93+
'Sentinel',
9294
'reveal_type',
9395
'runtime',
9496
'runtime_checkable',
@@ -4222,6 +4224,50 @@ def evaluate_forward_ref(
42224224
)
42234225

42244226

4227+
class Sentinel:
4228+
"""Create a unique sentinel object.
4229+
4230+
*name* should be the fully-qualified name of the variable to which the
4231+
return value shall be assigned.
4232+
4233+
*repr*, if supplied, will be used for the repr of the sentinel object.
4234+
If not provided, "<name>" will be used (with any leading class names
4235+
removed).
4236+
"""
4237+
4238+
def __init__(
4239+
self,
4240+
name: str,
4241+
repr: str | None = None,
4242+
):
4243+
self._name = name
4244+
self._repr = repr if repr is not None else f'<{name.split(".")[-1]}>'
4245+
4246+
def __repr__(self):
4247+
return self._repr
4248+
4249+
def __reduce__(self):
4250+
return (
4251+
type(self),
4252+
(
4253+
self._name,
4254+
self._repr,
4255+
)
4256+
)
4257+
4258+
if sys.version_info < (3, 11):
4259+
# The presence of this method convinces typing._type_check
4260+
# that Sentinels are types.
4261+
def __call__(self, *args, **kwargs):
4262+
raise TypeError(f"{type(self).__name__!r} object is not callable")
4263+
4264+
def __or__(self, other):
4265+
return Union[self, other]
4266+
4267+
def __ror__(self, other):
4268+
return Union[other, self]
4269+
4270+
42254271
# Aliases for items that are in typing in all supported versions.
42264272
# Explicitly assign these (rather than using `from typing import *` at the top),
42274273
# so that we get a CI error if one of these is deleted from typing.py

0 commit comments

Comments
 (0)