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
27 changes: 24 additions & 3 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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, ``"<name>"`` 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

Expand Down
50 changes: 41 additions & 9 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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), '<sentinel_no_repr>')

self.assertEqual(repr(Sentinel), "<class 'typing_extensions.Sentinel'>")

def test_sentinel_explicit_repr(self):
sentinel_explicit_repr = Sentinel('sentinel_explicit_repr', repr='explicit_repr')

Expand All @@ -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
Expand Down
60 changes: 34 additions & 26 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<name>" 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.
Expand Down
Loading