diff --git a/doc/index.rst b/doc/index.rst index e6ceb360..b581b7dc 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1056,25 +1056,46 @@ 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. + *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. + Sentinels can be tested using :ref:`is`, :func:`isinstance`, + or :ref:`match`. + Example:: >>> from typing_extensions import Sentinel, assert_type >>> MISSING = Sentinel('MISSING') - >>> def func(arg: int | MISSING = MISSING) -> None: + >>> def check_identity(arg: int | MISSING = MISSING) -> None: ... if arg is MISSING: ... assert_type(arg, MISSING) ... else: ... assert_type(arg, int) ... - >>> func(MISSING) + >>> check_identity(MISSING) + >>> def check_match(arg: int | MISSING = MISSING) -> None: + ... match arg: + ... case MISSING(): + ... assert_type(arg, MISSING) + ... case int() + ... assert_type(arg, int) + ... + >>> check_match(MISSING) + + Sentinels defined inside a class scope should use a :term:`qualified name`. + + Example:: + + >>> class MyClass: + ... MISSING = Sentinel('MyClass.MISSING') .. versionadded:: 4.14.0 diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index f07e1eb0..4bb769ba 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -9541,12 +9541,16 @@ def test_invalid_special_forms(self): class TestSentinels(BaseTestCase): + SENTINEL = Sentinel("TestSentinels.SENTINEL") + def test_sentinel_no_repr(self): sentinel_no_repr = Sentinel('sentinel_no_repr') - self.assertEqual(sentinel_no_repr._name, 'sentinel_no_repr') + self.assertEqual(sentinel_no_repr.__name__, 'sentinel_no_repr') self.assertEqual(repr(sentinel_no_repr), '') + self.assertEqual(repr(Sentinel), "") + def test_sentinel_explicit_repr(self): sentinel_explicit_repr = Sentinel('sentinel_explicit_repr', repr='explicit_repr') @@ -9566,17 +9570,45 @@ def test_sentinel_not_callable(self): sentinel = Sentinel('sentinel') with self.assertRaisesRegex( TypeError, - "'Sentinel' object is not callable" + f"Sentinel object {re.escape(repr(sentinel))} is not callable" ): sentinel() - def test_sentinel_not_picklable(self): - sentinel = Sentinel('sentinel') - with self.assertRaisesRegex( - TypeError, - "Cannot pickle 'Sentinel' object" - ): - pickle.dumps(sentinel) + def test_sentinel_copy_identity(self): + self.assertIs(self.SENTINEL, copy.copy(self.SENTINEL)) + self.assertIs(self.SENTINEL, copy.deepcopy(self.SENTINEL)) + + anonymous_sentinel = Sentinel("anonymous_sentinel") + self.assertIs(anonymous_sentinel, copy.copy(anonymous_sentinel)) + self.assertIs(anonymous_sentinel, copy.deepcopy(anonymous_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_isinstance(self): + anonymous_sentinel = Sentinel("anonymous_sentinel") + self.assertIsInstance(self.SENTINEL, self.SENTINEL) + self.assertIsInstance(anonymous_sentinel, anonymous_sentinel) + self.assertNotIsInstance(self.SENTINEL, anonymous_sentinel) + + self.assertIsInstance(self.SENTINEL, object) + self.assertIsInstance(self.SENTINEL, type) + self.assertNotIsInstance(self.SENTINEL, Sentinel) + + self.assertIsSubclass(self.SENTINEL, object) + self.assertIsSubclass(self.SENTINEL, Sentinel) + self.assertIsSubclass(self.SENTINEL, self.SENTINEL) + self.assertNotIsSubclass(self.SENTINEL, anonymous_sentinel) def load_tests(loader, tests, pattern): import doctest diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 9dd8eac4..9918f1b2 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -159,45 +159,53 @@ # Added with bpo-45166 to 3.10.1+ and some 3.9 versions _FORWARD_REF_HAS_CLASS = "__forward_is_class__" in typing.ForwardRef.__slots__ -class Sentinel: +class _SentinelMeta(type): + def __instancecheck__(self, instance) -> bool: + return self is instance + + def __repr__(self) -> str: + if self is Sentinel: + return super().__repr__() # self._repr missing on base class + return self._repr + + +class Sentinel(metaclass=_SentinelMeta): """Create a unique sentinel object. *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. """ - 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}>' - - def __repr__(self): - return self._repr - - if sys.version_info < (3, 11): - # The presence of this method convinces typing._type_check - # that Sentinels are types. - def __call__(self, *args, **kwargs): - raise TypeError(f"{type(self).__name__!r} object is not callable") - - # Breakpoint: https://github.com/python/cpython/pull/21515 - if sys.version_info >= (3, 10): - def __or__(self, other): - return typing.Union[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 stubbed_call(cls, *args, **kwargs): + raise TypeError(f"Sentinel object {cls!r} is not callable") + + repr = repr if repr is not None else f'<{name}>' + module_name = module_name if module_name is not None else _caller() + + return type( + name, + (cls,), + { + "__new__": stubbed_call, # Disable calling sentinel definitions + "_repr": repr, + "__module__": module_name, # For pickling + }, + ) -_marker = Sentinel("sentinel") +_marker = Sentinel("sentinel", __name__) # The functions below are modified copies of typing internal helpers. # They are needed by _ProtocolMeta and they provide support for PEP 646.