Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9570,13 +9570,24 @@ def test_sentinel_not_callable(self):
):
sentinel()

def test_sentinel_not_picklable(self):
def test_sentinel_picklable(self):
sentinel = Sentinel('sentinel')
with self.assertRaisesRegex(
TypeError,
"Cannot pickle 'Sentinel' object"
):
pickle.dumps(sentinel)
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
pickled = pickle.dumps(sentinel, protocol=proto)
loaded = pickle.loads(pickled)
self.assertIs(loaded, sentinel)

def test_sentinel_pickle_preserves_identity(self):
sentinel = Sentinel('pickle_identity_test')
pickled = pickle.dumps(sentinel)
loaded = pickle.loads(pickled)
self.assertIs(loaded, sentinel)
self.assertEqual(repr(loaded), '<pickle_identity_test>')

def test_sentinel_singleton(self):
s1 = Sentinel('singleton_test')
s2 = Sentinel('singleton_test')
self.assertIs(s1, s2)

def load_tests(loader, tests, pattern):
import doctest
Expand Down
35 changes: 26 additions & 9 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@
# Added with bpo-45166 to 3.10.1+ and some 3.9 versions
_FORWARD_REF_HAS_CLASS = "__forward_is_class__" in typing.ForwardRef.__slots__

_sentinel_registry = {}


class Sentinel:
"""Create a unique sentinel object.

Expand All @@ -168,13 +171,27 @@ class Sentinel:
If not provided, "<name>" will be used.
"""

def __init__(
self,
name: str,
repr: typing.Optional[str] = None,
):
self._name = name
self._repr = repr if repr is not None else f'<{name}>'
def __new__(cls, name: str, repr: typing.Optional[str] = None, module_name: typing.Optional[str] = None):
if module_name is None:
# Auto-detect calling module
frame = inspect.currentframe()
try:
caller = frame.f_back
module_name = caller.f_globals.get('__name__', '__main__')
finally:
del frame

key = (module_name, name)
existing = _sentinel_registry.get(key)
if existing is not None:
return existing

instance = super().__new__(cls)
instance._name = name
instance._repr = repr if repr is not None else f'<{name}>'
instance._module_name = module_name
_sentinel_registry[key] = instance
return instance

def __repr__(self):
return self._repr
Expand All @@ -193,8 +210,8 @@ def __or__(self, other):
def __ror__(self, other):
return typing.Union[other, self]

def __getstate__(self):
raise TypeError(f"Cannot pickle {type(self).__name__!r} object")
def __reduce__(self):
return (self.__class__, (self._name, None, self._module_name))


_marker = Sentinel("sentinel")
Expand Down