Skip to content

Commit df5650a

Browse files
Align sentinel implementation with PEP 661 (#617)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
1 parent 83caa59 commit df5650a

3 files changed

Lines changed: 158 additions & 65 deletions

File tree

doc/index.rst

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,18 +1071,17 @@ Capsule objects
10711071
Sentinel objects
10721072
~~~~~~~~~~~~~~~~
10731073

1074-
.. class:: Sentinel(name, repr=None)
1074+
.. class:: sentinel(name, /)
10751075

10761076
A type used to define sentinel values. The *name* argument should be the
10771077
name of the variable to which the return value shall be assigned.
10781078

1079-
If *repr* is provided, it will be used for the :meth:`~object.__repr__`
1080-
of the sentinel object. If not provided, ``"<name>"`` will be used.
1079+
Assigning attributes to a sentinel is deprecated.
10811080

10821081
Example::
10831082

1084-
>>> from typing_extensions import Sentinel, assert_type
1085-
>>> MISSING = Sentinel('MISSING')
1083+
>>> from typing_extensions import sentinel, assert_type
1084+
>>> MISSING = sentinel('MISSING')
10861085
>>> def func(arg: int | MISSING = MISSING) -> None:
10871086
... if arg is MISSING:
10881087
... assert_type(arg, MISSING)
@@ -1095,6 +1094,18 @@ Sentinel objects
10951094

10961095
See :pep:`661`
10971096

1097+
.. versionchanged:: 4.16.0
1098+
1099+
The implementation of this class has been updated to conform to
1100+
the accepted version of :pep:`661`.
1101+
1102+
Now supports pickle and will be reduced as a singleton.
1103+
Renamed from `Sentinel` to `sentinel`, `Sentinel` is deprecated.
1104+
Automatic `repr` string no longer has angle brackets.
1105+
`repr` parameter was deprecated.
1106+
`name` as a keyword is deprecated.
1107+
Subclassing and attribute assignment are deprecated.
1108+
10981109

10991110
Pure aliases
11001111
~~~~~~~~~~~~

src/test_typing_extensions.py

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
reveal_type,
103103
runtime,
104104
runtime_checkable,
105+
sentinel,
105106
type_repr,
106107
)
107108

@@ -9541,42 +9542,71 @@ def test_invalid_special_forms(self):
95419542

95429543

95439544
class TestSentinels(BaseTestCase):
9545+
SENTINEL = sentinel("TestSentinels.SENTINEL")
9546+
95449547
def test_sentinel_no_repr(self):
9545-
sentinel_no_repr = Sentinel('sentinel_no_repr')
9548+
sentinel_no_repr = sentinel('sentinel_no_repr')
95469549

9547-
self.assertEqual(sentinel_no_repr._name, 'sentinel_no_repr')
9548-
self.assertEqual(repr(sentinel_no_repr), '<sentinel_no_repr>')
9550+
self.assertEqual(sentinel_no_repr.__name__, 'sentinel_no_repr')
9551+
self.assertEqual(repr(sentinel_no_repr), 'sentinel_no_repr')
95499552

9550-
def test_sentinel_explicit_repr(self):
9551-
sentinel_explicit_repr = Sentinel('sentinel_explicit_repr', repr='explicit_repr')
9553+
def test_sentinel_deprecated_explicit_repr(self):
9554+
with self.assertWarnsRegex(DeprecationWarning, r"'repr' parameter is deprecated and will be removed"):
9555+
sentinel_explicit_repr = sentinel('sentinel_explicit_repr', repr='explicit_repr')
95529556

95539557
self.assertEqual(repr(sentinel_explicit_repr), 'explicit_repr')
95549558

95559559
@skipIf(sys.version_info < (3, 10), reason='New unions not available in 3.9')
95569560
def test_sentinel_type_expression_union(self):
9557-
sentinel = Sentinel('sentinel')
9561+
sentinel_type = sentinel('sentinel')
95589562

9559-
def func1(a: int | sentinel = sentinel): pass
9560-
def func2(a: sentinel | int = sentinel): pass
9563+
def func1(a: int | sentinel_type = sentinel_type): pass
9564+
def func2(a: sentinel_type | int = sentinel_type): pass
95619565

9562-
self.assertEqual(func1.__annotations__['a'], Union[int, sentinel])
9563-
self.assertEqual(func2.__annotations__['a'], Union[sentinel, int])
9566+
self.assertEqual(func1.__annotations__['a'], Union[int, sentinel_type])
9567+
self.assertEqual(func2.__annotations__['a'], Union[sentinel_type, int])
95649568

95659569
def test_sentinel_not_callable(self):
9566-
sentinel = Sentinel('sentinel')
9570+
sentinel_ = sentinel('sentinel')
95679571
with self.assertRaisesRegex(
95689572
TypeError,
9569-
"'Sentinel' object is not callable"
9573+
"'sentinel' object is not callable"
95709574
):
9575+
sentinel_()
9576+
9577+
def test_sentinel_copy_identity(self):
9578+
self.assertIs(self.SENTINEL, copy.copy(self.SENTINEL))
9579+
self.assertIs(self.SENTINEL, copy.deepcopy(self.SENTINEL))
9580+
9581+
anonymous_sentinel = sentinel("anonymous_sentinel")
9582+
self.assertIs(anonymous_sentinel, copy.copy(anonymous_sentinel))
9583+
self.assertIs(anonymous_sentinel, copy.deepcopy(anonymous_sentinel))
9584+
9585+
def test_sentinel_picklable_qualified(self):
9586+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
9587+
self.assertIs(self.SENTINEL, pickle.loads(pickle.dumps(self.SENTINEL, protocol=proto)))
9588+
9589+
def test_sentinel_picklable_anonymous(self):
9590+
anonymous_sentinel = sentinel("anonymous_sentinel") # Anonymous sentinel can not be pickled
9591+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
9592+
with self.assertRaisesRegex(
9593+
pickle.PicklingError,
9594+
r"attribute lookup anonymous_sentinel on \w+ failed|not found as \w+.anonymous_sentinel"
9595+
):
9596+
self.assertIs(anonymous_sentinel, pickle.loads(pickle.dumps(anonymous_sentinel, protocol=proto)))
9597+
9598+
def test_sentinel_deprecated(self):
9599+
with self.assertWarnsRegex(DeprecationWarning, r"Subclassing sentinel is deprecated"):
9600+
class SentinelSubclass(Sentinel):
9601+
pass
9602+
with self.assertRaisesRegex(TypeError, r"First parameter 'name' is required"):
95719603
sentinel()
95729604

9573-
def test_sentinel_not_picklable(self):
9574-
sentinel = Sentinel('sentinel')
9575-
with self.assertRaisesRegex(
9576-
TypeError,
9577-
"Cannot pickle 'Sentinel' object"
9578-
):
9579-
pickle.dumps(sentinel)
9605+
with self.assertWarnsRegex(DeprecationWarning, r"Passing 'name' as a keyword argument is deprecated"):
9606+
my_sentinel = Sentinel(name="my_sentinel")
9607+
with self.assertWarnsRegex(DeprecationWarning, r"Setting attribute 'foo' on sentinel objects is deprecated"):
9608+
my_sentinel.foo = "bar"
9609+
95809610

95819611
def load_tests(loader, tests, pattern):
95829612
import doctest

src/typing_extensions.py

Lines changed: 93 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
'overload',
9292
'override',
9393
'Protocol',
94+
'sentinel',
9495
'Sentinel',
9596
'reveal_type',
9697
'runtime',
@@ -159,45 +160,108 @@
159160
# Added with bpo-45166 to 3.10.1+ and some 3.9 versions
160161
_FORWARD_REF_HAS_CLASS = "__forward_is_class__" in typing.ForwardRef.__slots__
161162

162-
class Sentinel:
163-
"""Create a unique sentinel object.
164163

165-
*name* should be the name of the variable to which the return value shall be assigned.
164+
def _caller(depth=1, default='__main__'):
165+
try:
166+
return sys._getframemodulename(depth + 1) or default
167+
except AttributeError: # For platforms without _getframemodulename()
168+
pass
169+
try:
170+
return sys._getframe(depth + 1).f_globals.get('__name__', default)
171+
except (AttributeError, ValueError): # For platforms without _getframe()
172+
pass
173+
return None
166174

167-
*repr*, if supplied, will be used for the repr of the sentinel object.
168-
If not provided, "<name>" will be used.
169-
"""
170175

171-
def __init__(
172-
self,
173-
name: str,
174-
repr: typing.Optional[str] = None,
175-
):
176-
self._name = name
177-
self._repr = repr if repr is not None else f'<{name}>'
176+
# Placeholder for sentinel methods, because sentinels can not have their own sentinels
177+
_sentinel_placeholder = object()
178178

179-
def __repr__(self):
180-
return self._repr
179+
if hasattr(builtins, "sentinel"): # 3.15+
180+
sentinel = builtins.sentinel
181+
else:
182+
class sentinel:
183+
"""Create a unique sentinel object.
181184
182-
if sys.version_info < (3, 11):
183-
# The presence of this method convinces typing._type_check
184-
# that Sentinels are types.
185-
def __call__(self, *args, **kwargs):
186-
raise TypeError(f"{type(self).__name__!r} object is not callable")
185+
*name* should be the name of the variable to which the return value
186+
shall be assigned.
187+
"""
188+
189+
def __init__(
190+
self,
191+
__name: str = _sentinel_placeholder,
192+
/,
193+
repr: typing.Optional[str] = None,
194+
*,
195+
name: str = _sentinel_placeholder,
196+
) -> None:
197+
if name is not _sentinel_placeholder:
198+
warnings.warn(
199+
"Passing 'name' as a keyword argument is deprecated; "
200+
"pass it positionally instead.",
201+
DeprecationWarning,
202+
stacklevel=2,
203+
)
204+
__name = name
205+
if __name is _sentinel_placeholder:
206+
raise TypeError("First parameter 'name' is required")
207+
if repr is not None:
208+
warnings.warn(
209+
"The 'repr' parameter is deprecated "
210+
"and will be removed in Python 3.15.",
211+
DeprecationWarning,
212+
stacklevel=2,
213+
)
214+
215+
self.__name__ = __name
216+
self._repr = repr if repr is not None else __name
217+
218+
# For pickling as a singleton:
219+
self.__module__ = _caller()
220+
221+
def __init_subclass__(cls):
222+
warnings.warn(
223+
"Subclassing sentinel is deprecated "
224+
"and will be disallowed in Python 3.15",
225+
DeprecationWarning,
226+
stacklevel=2,
227+
)
228+
super().__init_subclass__()
229+
230+
def __setattr__(self, attr: str, value: object) -> None:
231+
if attr not in {"__name__", "_repr", "__module__"}:
232+
warnings.warn(
233+
f"Setting attribute {attr!r} on sentinel objects is deprecated "
234+
"and will be disallowed in Python 3.15.",
235+
DeprecationWarning,
236+
stacklevel=2,
237+
)
238+
super().__setattr__(attr, value)
239+
240+
def __repr__(self):
241+
return self._repr
242+
243+
if sys.version_info < (3, 11):
244+
# The presence of this method convinces typing._type_check
245+
# that Sentinels are types.
246+
def __call__(self, *args, **kwargs):
247+
raise TypeError(f"{type(self).__name__!r} object is not callable")
248+
249+
# Breakpoint: https://github.com/python/cpython/pull/21515
250+
if sys.version_info >= (3, 10):
251+
def __or__(self, other):
252+
return typing.Union[self, other]
187253

188-
# Breakpoint: https://github.com/python/cpython/pull/21515
189-
if sys.version_info >= (3, 10):
190-
def __or__(self, other):
191-
return typing.Union[self, other]
254+
def __ror__(self, other):
255+
return typing.Union[other, self]
192256

193-
def __ror__(self, other):
194-
return typing.Union[other, self]
257+
def __reduce__(self) -> str:
258+
"""Reduce this sentinel to a singleton."""
259+
return self.__name__ # Module is taken from the __module__ attribute
195260

196-
def __getstate__(self):
197-
raise TypeError(f"Cannot pickle {type(self).__name__!r} object")
261+
Sentinel = sentinel
198262

263+
_marker = sentinel("sentinel")
199264

200-
_marker = Sentinel("sentinel")
201265

202266
# The functions below are modified copies of typing internal helpers.
203267
# They are needed by _ProtocolMeta and they provide support for PEP 646.
@@ -638,18 +702,6 @@ def _get_protocol_attrs(cls):
638702
return attrs
639703

640704

641-
def _caller(depth=1, default='__main__'):
642-
try:
643-
return sys._getframemodulename(depth + 1) or default
644-
except AttributeError: # For platforms without _getframemodulename()
645-
pass
646-
try:
647-
return sys._getframe(depth + 1).f_globals.get('__name__', default)
648-
except (AttributeError, ValueError): # For platforms without _getframe()
649-
pass
650-
return None
651-
652-
653705
# `__match_args__` attribute was removed from protocol members in 3.13,
654706
# we want to backport this change to older Python versions.
655707
# Breakpoint: https://github.com/python/cpython/pull/110683

0 commit comments

Comments
 (0)