Skip to content

Commit f68007e

Browse files
committed
Update sentinel for PEP 661 changes
Rename Sentinel to sentinel, deprecated old name Remove module_name parameter Deprecate subclassing sentinel Enforce correct sentinel parameters using deprecated overloads Rename _name attribute to __name__ Rename sentinel in tests, tests passed before making this change Also add tests for sentinel deprecations
1 parent 9ba60f0 commit f68007e

3 files changed

Lines changed: 68 additions & 33 deletions

File tree

doc/index.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,21 +1071,21 @@ Capsule objects
10711071
Sentinel objects
10721072
~~~~~~~~~~~~~~~~
10731073

1074-
.. class:: Sentinel(name, *, module_name=None, repr=None)
1074+
.. class:: sentinel(name, /, *, repr=None)
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-
*module_name* is the module where the sentinel is defined.
1080-
Defaults to the current modules ``__name__``.
1081-
10821079
If *repr* is provided, it will be used for the :meth:`~object.__repr__`
10831080
of the sentinel object. If not provided, ``"<name>"`` will be used.
10841081

1082+
A sentinel is bound to the module it is created within,
1083+
sentinels are not equal to similar named sentinels from other modules.
1084+
10851085
Example::
10861086

1087-
>>> from typing_extensions import Sentinel, assert_type
1088-
>>> MISSING = Sentinel('MISSING')
1087+
>>> from typing_extensions import sentinel, assert_type
1088+
>>> MISSING = sentinel('MISSING')
10891089
>>> def func(arg: int | MISSING = MISSING) -> None:
10901090
... if arg is MISSING:
10911091
... assert_type(arg, MISSING)
@@ -1099,7 +1099,7 @@ Sentinel objects
10991099
Example::
11001100

11011101
>>> class MyClass:
1102-
... MISSING = Sentinel('MyClass.MISSING')
1102+
... MISSING = sentinel('MyClass.MISSING')
11031103

11041104
.. versionadded:: 4.14.0
11051105

src/test_typing_extensions.py

Lines changed: 27 additions & 15 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,42 @@ def test_invalid_special_forms(self):
95419542

95429543

95439544
class TestSentinels(BaseTestCase):
9544-
SENTINEL = Sentinel("TestSentinels.SENTINEL")
9545+
SENTINEL = sentinel("TestSentinels.SENTINEL")
95459546

95469547
def test_sentinel_no_repr(self):
9547-
sentinel_no_repr = Sentinel('sentinel_no_repr')
9548+
sentinel_no_repr = sentinel('sentinel_no_repr')
95489549

9549-
self.assertEqual(sentinel_no_repr._name, 'sentinel_no_repr')
9550-
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')
95519552

95529553
def test_sentinel_explicit_repr(self):
9553-
sentinel_explicit_repr = Sentinel('sentinel_explicit_repr', repr='explicit_repr')
9554+
sentinel_explicit_repr = sentinel('sentinel_explicit_repr', repr='explicit_repr')
95549555

95559556
self.assertEqual(repr(sentinel_explicit_repr), 'explicit_repr')
95569557

95579558
@skipIf(sys.version_info < (3, 10), reason='New unions not available in 3.9')
95589559
def test_sentinel_type_expression_union(self):
9559-
sentinel = Sentinel('sentinel')
9560+
sentinel_type = sentinel('sentinel')
95609561

9561-
def func1(a: int | sentinel = sentinel): pass
9562-
def func2(a: sentinel | int = sentinel): pass
9562+
def func1(a: int | sentinel_type = sentinel_type): pass
9563+
def func2(a: sentinel_type | int = sentinel_type): pass
95639564

9564-
self.assertEqual(func1.__annotations__['a'], Union[int, sentinel])
9565-
self.assertEqual(func2.__annotations__['a'], Union[sentinel, int])
9565+
self.assertEqual(func1.__annotations__['a'], Union[int, sentinel_type])
9566+
self.assertEqual(func2.__annotations__['a'], Union[sentinel_type, int])
95669567

95679568
def test_sentinel_not_callable(self):
9568-
sentinel = Sentinel('sentinel')
9569+
sentinel_ = sentinel('sentinel')
95699570
with self.assertRaisesRegex(
95709571
TypeError,
9571-
"'Sentinel' object is not callable"
9572+
"'sentinel' object is not callable"
95729573
):
9573-
sentinel()
9574+
sentinel_()
95749575

95759576
def test_sentinel_copy_identity(self):
95769577
self.assertIs(self.SENTINEL, copy.copy(self.SENTINEL))
95779578
self.assertIs(self.SENTINEL, copy.deepcopy(self.SENTINEL))
95789579

9579-
anonymous_sentinel = Sentinel("anonymous_sentinel")
9580+
anonymous_sentinel = sentinel("anonymous_sentinel")
95809581
self.assertIs(anonymous_sentinel, copy.copy(anonymous_sentinel))
95819582
self.assertIs(anonymous_sentinel, copy.deepcopy(anonymous_sentinel))
95829583

@@ -9585,14 +9586,25 @@ def test_sentinel_picklable_qualified(self):
95859586
self.assertIs(self.SENTINEL, pickle.loads(pickle.dumps(self.SENTINEL, protocol=proto)))
95869587

95879588
def test_sentinel_picklable_anonymous(self):
9588-
anonymous_sentinel = Sentinel("anonymous_sentinel") # Anonymous sentinel can not be pickled
9589+
anonymous_sentinel = sentinel("anonymous_sentinel") # Anonymous sentinel can not be pickled
95899590
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
95909591
with self.assertRaisesRegex(
95919592
pickle.PicklingError,
95929593
r"attribute lookup anonymous_sentinel on \w+ failed|not found as \w+.anonymous_sentinel"
95939594
):
95949595
self.assertIs(anonymous_sentinel, pickle.loads(pickle.dumps(anonymous_sentinel, protocol=proto)))
95959596

9597+
def test_sentinel_deprecated(self):
9598+
with self.assertWarnsRegex(DeprecationWarning, r"Subclassing sentinel is forbidden by PEP 661"):
9599+
class SentinelSubclass(Sentinel):
9600+
pass
9601+
9602+
with self.assertWarnsRegex(DeprecationWarning, r"Sentinel was renamed to typing_extensions.sentinel"):
9603+
my_sentinel = Sentinel(name="my_sentinel")
9604+
with self.assertWarnsRegex(DeprecationWarning, r"Setting attributes on sentinel is deprecated"):
9605+
my_sentinel.foo = "bar"
9606+
9607+
95969608
def load_tests(loader, tests, pattern):
95979609
import doctest
95989610
tests.addTests(doctest.DocTestSuite(typing_extensions))

src/typing_extensions.py

Lines changed: 34 additions & 11 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',
@@ -386,30 +387,45 @@ def _caller(depth=1, default='__main__'):
386387
return None
387388

388389

389-
class Sentinel:
390+
class sentinel:
390391
"""Create a unique sentinel object.
391392
392393
*name* should be the name of the variable to which the return value shall be assigned.
393394
394-
*module_name* is the module where the sentinel is defined.
395-
Defaults to the current modules ``__name__``.
396-
397395
*repr*, if supplied, will be used for the repr of the sentinel object.
398396
If not provided, "<name>" will be used.
399397
"""
400398

399+
@overload
400+
def __init__(self, name: str, /, *, repr: typing.Optional[str] = None): ...
401+
402+
@overload
403+
@deprecated("'name' must be positional-only, 'repr' must be keyword-only.")
404+
def __init__(self, name: str, repr: typing.Optional[str] = None): ...
405+
401406
def __init__(
402407
self,
403408
name: str,
404-
*,
405-
module_name: typing.Optional[str] = None,
406409
repr: typing.Optional[str] = None,
407410
):
408-
self._name = name
409-
self._repr = repr if repr is not None else f'<{name}>'
411+
self.__name__ = name
412+
self._repr = repr if repr is not None else name
410413

411414
# For pickling as a singleton:
412-
self.__module__ = module_name if module_name is not None else _caller()
415+
self.__module__ = _caller()
416+
417+
@deprecated("Subclassing sentinel is forbidden by PEP 661")
418+
def __init_subclass__(cls):
419+
super().__init_subclass__()
420+
421+
def __setattr__(self, attr: str, value: object) -> None:
422+
if attr not in {"__name__", "_repr", "__module__"}:
423+
warnings.warn(
424+
"Setting attributes on sentinel is deprecated",
425+
DeprecationWarning,
426+
stacklevel=2,
427+
)
428+
super().__setattr__(attr, value)
413429

414430
def __repr__(self):
415431
return self._repr
@@ -430,10 +446,17 @@ def __ror__(self, other):
430446

431447
def __reduce__(self) -> str:
432448
"""Reduce this sentinel to a singleton."""
433-
return self._name # Module is taken from the __module__ attribute
449+
return self.__name__ # Module is taken from the __module__ attribute
450+
451+
with warnings.catch_warnings(): # Allow sentinel subclass for backwards compatibility
452+
warnings.simplefilter("ignore")
453+
454+
@deprecated("""Sentinel was renamed to typing_extensions.sentinel""")
455+
class Sentinel(sentinel):
456+
pass
434457

458+
_marker = sentinel("sentinel")
435459

436-
_marker = Sentinel("sentinel", module_name=__name__)
437460

438461
# The functions below are modified copies of typing internal helpers.
439462
# They are needed by _ProtocolMeta and they provide support for PEP 646.

0 commit comments

Comments
 (0)