Skip to content

Commit a3ff798

Browse files
author
Contributor
committed
Add pickle support for Sentinel via singleton registry
Sentinel objects now support pickling/unpickling using a module-level registry keyed by (module_name, name). Unpickled sentinels preserve object identity (is checks pass). Changes: - Add _sentinel_registry dict for singleton tracking - Convert __init__ to __new__ with registry-based singleton pattern - Replace __getstate__ (which raised TypeError) with __reduce__ - __reduce__ returns (cls, (name, None, module_name)) for reconstruction - Auto-detect calling module via inspect.currentframe() - Update tests: verify pickle roundtrip, identity preservation, singleton Implements the approach discussed in issue #720, following the PEP 661 reference implementation's singleton registry pattern. Fixes #720
1 parent 442d848 commit a3ff798

File tree

2 files changed

+43
-15
lines changed

2 files changed

+43
-15
lines changed

src/test_typing_extensions.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9570,13 +9570,24 @@ def test_sentinel_not_callable(self):
95709570
):
95719571
sentinel()
95729572

9573-
def test_sentinel_not_picklable(self):
9573+
def test_sentinel_picklable(self):
95749574
sentinel = Sentinel('sentinel')
9575-
with self.assertRaisesRegex(
9576-
TypeError,
9577-
"Cannot pickle 'Sentinel' object"
9578-
):
9579-
pickle.dumps(sentinel)
9575+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
9576+
pickled = pickle.dumps(sentinel, protocol=proto)
9577+
loaded = pickle.loads(pickled)
9578+
self.assertIs(loaded, sentinel)
9579+
9580+
def test_sentinel_pickle_preserves_identity(self):
9581+
sentinel = Sentinel('pickle_identity_test')
9582+
pickled = pickle.dumps(sentinel)
9583+
loaded = pickle.loads(pickled)
9584+
self.assertIs(loaded, sentinel)
9585+
self.assertEqual(repr(loaded), '<pickle_identity_test>')
9586+
9587+
def test_sentinel_singleton(self):
9588+
s1 = Sentinel('singleton_test')
9589+
s2 = Sentinel('singleton_test')
9590+
self.assertIs(s1, s2)
95809591

95819592
def load_tests(loader, tests, pattern):
95829593
import doctest

src/typing_extensions.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@
159159
# Added with bpo-45166 to 3.10.1+ and some 3.9 versions
160160
_FORWARD_REF_HAS_CLASS = "__forward_is_class__" in typing.ForwardRef.__slots__
161161

162+
_sentinel_registry = {}
163+
164+
162165
class Sentinel:
163166
"""Create a unique sentinel object.
164167
@@ -168,13 +171,27 @@ class Sentinel:
168171
If not provided, "<name>" will be used.
169172
"""
170173

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}>'
174+
def __new__(cls, name: str, repr: typing.Optional[str] = None, module_name: typing.Optional[str] = None):
175+
if module_name is None:
176+
# Auto-detect calling module
177+
frame = inspect.currentframe()
178+
try:
179+
caller = frame.f_back
180+
module_name = caller.f_globals.get('__name__', '__main__')
181+
finally:
182+
del frame
183+
184+
key = (module_name, name)
185+
existing = _sentinel_registry.get(key)
186+
if existing is not None:
187+
return existing
188+
189+
instance = super().__new__(cls)
190+
instance._name = name
191+
instance._repr = repr if repr is not None else f'<{name}>'
192+
instance._module_name = module_name
193+
_sentinel_registry[key] = instance
194+
return instance
178195

179196
def __repr__(self):
180197
return self._repr
@@ -193,8 +210,8 @@ def __or__(self, other):
193210
def __ror__(self, other):
194211
return typing.Union[other, self]
195212

196-
def __getstate__(self):
197-
raise TypeError(f"Cannot pickle {type(self).__name__!r} object")
213+
def __reduce__(self):
214+
return (self.__class__, (self._name, None, self._module_name))
198215

199216

200217
_marker = Sentinel("sentinel")

0 commit comments

Comments
 (0)