diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index f07e1eb0..8073d91c 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -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), '') + + 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 diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 20c331ee..21118dc5 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -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. @@ -168,13 +171,27 @@ class Sentinel: If not provided, "" 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 @@ -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")