diff --git a/doc/index.rst b/doc/index.rst index 21d6fa60..d3d96315 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1030,13 +1030,25 @@ Capsule objects Sentinel objects ~~~~~~~~~~~~~~~~ -.. class:: Sentinel(name, repr=None) +.. class:: Sentinel(name, module_name=None, *, repr=None) - A type used to define sentinel values. The *name* argument should be the - name of the variable to which the return value shall be assigned. + A type used to define custom sentinel values. + + *name* should be the qualified name of the variable to which + the return value shall be assigned. + + *module_name* is the module where the sentinel is defined. + Defaults to the current modules ``__name__``. If *repr* is provided, it will be used for the :meth:`~object.__repr__` - of the sentinel object. If not provided, ``""`` will be used. + of the sentinel object. If not provided, *name* will be used. + + All sentinels with the same *name* and *module_name* have the same identity. + Sentinel identity is preserved across :py:mod:`copy` and :py:mod:`pickle`. + + Sentinel objects are tested using :py:ref:`is`. + Sentinels have no truthiness and attempting to convert a sentinel to + :py:class:`bool` will raise :py:exc:`TypeError`. Example:: @@ -1050,6 +1062,13 @@ Sentinel objects ... >>> func(MISSING) + Sentinels defined in a class scope must use fully qualified names. + + Example:: + + >>> class MyClass: + ... MISSING = Sentinel('MyClass.MISSING') + .. versionadded:: 4.14.0 See :pep:`661` diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 5de161f9..34add814 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -9269,16 +9269,33 @@ def test_invalid_special_forms(self): class TestSentinels(BaseTestCase): - def test_sentinel_no_repr(self): - sentinel_no_repr = Sentinel('sentinel_no_repr') + SENTINEL = Sentinel("TestSentinels.SENTINEL") - self.assertEqual(sentinel_no_repr._name, 'sentinel_no_repr') - self.assertEqual(repr(sentinel_no_repr), '') + def test_sentinel_repr(self): + self.assertEqual(repr(TestSentinels.SENTINEL), "TestSentinels.SENTINEL") + self.assertEqual(repr(Sentinel("sentinel")), "sentinel") def test_sentinel_explicit_repr(self): - sentinel_explicit_repr = Sentinel('sentinel_explicit_repr', repr='explicit_repr') + sentinel_explicit_repr = Sentinel("sentinel_explicit_repr", repr="explicit_repr") + self.assertEqual(repr(sentinel_explicit_repr), "explicit_repr") + with self.assertWarnsRegex( + DeprecationWarning, + r"repr='sentinel_explicit_repr' conflicts with initial definition of repr='explicit_repr'" + ): + self.assertEqual(repr(Sentinel("sentinel_explicit_repr")), "explicit_repr") - self.assertEqual(repr(sentinel_explicit_repr), 'explicit_repr') + def test_sentinel_explicit_repr_deprecated(self): + with self.assertWarnsRegex( + DeprecationWarning, + r"Use keyword parameter repr='explicit_repr' instead" + ): + deprecated_repr = Sentinel("deprecated_repr", "explicit_repr") + self.assertEqual(repr(deprecated_repr), "explicit_repr") + with self.assertWarnsRegex( + DeprecationWarning, + r"repr='deprecated_repr' conflicts with initial definition of repr='explicit_repr'" + ): + self.assertEqual(repr(Sentinel("deprecated_repr")), "explicit_repr") @skipIf(sys.version_info < (3, 10), reason='New unions not available in 3.9') def test_sentinel_type_expression_union(self): @@ -9298,13 +9315,33 @@ def test_sentinel_not_callable(self): ): sentinel() - def test_sentinel_not_picklable(self): - sentinel = Sentinel('sentinel') + def test_sentinel_identity(self): + self.assertIs(TestSentinels.SENTINEL, Sentinel("TestSentinels.SENTINEL")) + self.assertIs(Sentinel("SENTINEL"), Sentinel("SENTINEL", __name__)) + self.assertIsNot(TestSentinels.SENTINEL, Sentinel("SENTINEL")) + + def test_sentinel_copy(self): + self.assertIs(self.SENTINEL, copy.copy(self.SENTINEL)) + self.assertIs(self.SENTINEL, copy.deepcopy(self.SENTINEL)) + + def test_sentinel_picklable_qualified(self): + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + self.assertIs(self.SENTINEL, pickle.loads(pickle.dumps(self.SENTINEL, protocol=proto))) + + def test_sentinel_picklable_anonymous(self): + anonymous_sentinel = Sentinel("anonymous_sentinel") # Anonymous sentinel can not be pickled + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.assertRaisesRegex( + pickle.PicklingError, + r"attribute lookup anonymous_sentinel on \w+ failed|not found as \w+.anonymous_sentinel" + ): + self.assertIs(anonymous_sentinel, pickle.loads(pickle.dumps(anonymous_sentinel, protocol=proto))) + + def test_sentinel_bool(self): with self.assertRaisesRegex( - TypeError, - "Cannot pickle 'Sentinel' object" + TypeError, rf"{self.SENTINEL!r} is not convertable to bool", ): - pickle.dumps(sentinel) + bool(self.SENTINEL) if __name__ == '__main__': diff --git a/src/typing_extensions.py b/src/typing_extensions.py index efa09d55..74d5ffcd 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -5,6 +5,7 @@ import contextlib import enum import functools +import importlib import inspect import io import keyword @@ -4155,24 +4156,78 @@ def evaluate_forward_ref( ) +_sentinel_registry = {} + class Sentinel: - """Create a unique sentinel object. + """A sentinel object. + + *name* should be the qualified name of the variable to which + the return value shall be assigned. - *name* should be the name of the variable to which the return value shall be assigned. + *module_name* is the module where the sentinel is defined. + Defaults to the current modules ``__name__``. *repr*, if supplied, will be used for the repr of the sentinel object. - If not provided, "" will be used. + If not provided, *name* will be used. + + All sentinels with the same *name* and *module_name* have the same identity. + The ``is`` operator is used to test if an object is a sentinel. + Sentinel identity is preserved across copy and pickle. """ - def __init__( - self, + def __new__( + cls, name: str, + module_name: typing.Optional[str] = None, + *, repr: typing.Optional[str] = None, ): - self._name = name - self._repr = repr if repr is not None else f'<{name}>' + """Return an object with a consistent identity.""" + if module_name is not None and repr is None: + # 'repr' used to be the 2nd positional argument but is now 'module_name' + # Test if 'module_name' is a module or is the old 'repr' argument + # Use 'repr=name' to suppress this check + try: + importlib.import_module(module_name) + except Exception: + repr = module_name + module_name = None + warnings.warn( + "'repr' as a positional argument could be mistaken for the sentinels" + " 'module_name'." + f" Use keyword parameter repr={repr!r} instead.", + category=DeprecationWarning, + stacklevel=2, + ) - def __repr__(self): + if module_name is None: + module_name = _caller(default="") + + registry_key = f"{module_name}-{name}" + + repr = repr if repr is not None else name + + # Check registered sentinels + sentinel = _sentinel_registry.get(registry_key, None) + if sentinel is not None: + if sentinel._repr != repr: + warnings.warn( + f"repr={repr!r} conflicts with initial definition of " + f"repr={sentinel._repr!r} and will be ignored" + "\nUsage of repr should be consistent across definitions", + DeprecationWarning, + stacklevel=2, + ) + return sentinel + + # Create initial or anonymous sentinel + sentinel = super().__new__(cls) + sentinel._name = name + sentinel.__module__ = module_name # Assign which module defined this instance + sentinel._repr = repr + return _sentinel_registry.setdefault(registry_key, sentinel) + + def __repr__(self) -> str: return self._repr if sys.version_info < (3, 11): @@ -4188,8 +4243,13 @@ 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) -> str: + """Reduce this sentinel to a singleton.""" + return self._name # Module is set from __module__ attribute + + def __bool__(self) -> Never: + """Raise TypeError.""" + raise TypeError(f"Sentinel {self!r} is not convertable to bool.") # Aliases for items that are in typing in all supported versions. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy