Skip to content
Open
18 changes: 13 additions & 5 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1071,18 +1071,17 @@ Capsule objects
Sentinel objects
~~~~~~~~~~~~~~~~

.. class:: Sentinel(name, repr=None)
.. class:: sentinel(name, /)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should keep it named as Sentinel, I don't think it's worth trying to rename it. We'll just have the slightly odd case that typing_extensions.Sentinel == builtins.sentinel.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the correct name is important. Switching from typing_extensions to native Python and backporting to older versions should be a simple addition or removal of from typing_extensions import sentinel. Using Sentinel with the wrong case needs to be actively discouraged or else things will get worse for code migration.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, I'd like to hear more opinions on this though. I'll post in the issue.


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.

If *repr* is provided, it will be used for the :meth:`~object.__repr__`
of the sentinel object. If not provided, ``"<name>"`` will be used.
Assigning attributes to a sentinel including `__weakref__` is forbidden.
Comment thread
HexDecimal marked this conversation as resolved.
Outdated

Example::

>>> from typing_extensions import Sentinel, assert_type
>>> MISSING = Sentinel('MISSING')
>>> from typing_extensions import sentinel, assert_type
>>> MISSING = sentinel('MISSING')
>>> def func(arg: int | MISSING = MISSING) -> None:
... if arg is MISSING:
... assert_type(arg, MISSING)
Expand All @@ -1095,6 +1094,15 @@ Sentinel objects

See :pep:`661`

Comment thread
HexDecimal marked this conversation as resolved.
.. versionchanged:: 4.16.0

Now supports pickle and will be reduced as a singleton.
Comment thread
HexDecimal marked this conversation as resolved.
Renamed from `Sentinel` to `sentinel`, `Sentinel` is deprecated.
Automatic `repr` string no longer has angle brackets.
`repr` parameter was deprecated.
`name` as a keyword is deprecated.
Subclasssing and attribute assignment are deprecated.
Comment thread
HexDecimal marked this conversation as resolved.
Outdated


Pure aliases
~~~~~~~~~~~~
Expand Down
68 changes: 49 additions & 19 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
reveal_type,
runtime,
runtime_checkable,
sentinel,
type_repr,
)

Expand Down Expand Up @@ -9541,42 +9542,71 @@ 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')
sentinel_no_repr = sentinel('sentinel_no_repr')

self.assertEqual(sentinel_no_repr._name, 'sentinel_no_repr')
self.assertEqual(repr(sentinel_no_repr), '<sentinel_no_repr>')
self.assertEqual(sentinel_no_repr.__name__, 'sentinel_no_repr')
self.assertEqual(repr(sentinel_no_repr), 'sentinel_no_repr')

def test_sentinel_explicit_repr(self):
sentinel_explicit_repr = Sentinel('sentinel_explicit_repr', repr='explicit_repr')
def test_sentinel_deprecated_explicit_repr(self):
with self.assertWarnsRegex(DeprecationWarning, r"'repr' is deprecated and must be removed"):
sentinel_explicit_repr = sentinel('sentinel_explicit_repr', repr='explicit_repr')

self.assertEqual(repr(sentinel_explicit_repr), 'explicit_repr')

@skipIf(sys.version_info < (3, 10), reason='New unions not available in 3.9')
def test_sentinel_type_expression_union(self):
sentinel = Sentinel('sentinel')
sentinel_type = sentinel('sentinel')

def func1(a: int | sentinel = sentinel): pass
def func2(a: sentinel | int = sentinel): pass
def func1(a: int | sentinel_type = sentinel_type): pass
def func2(a: sentinel_type | int = sentinel_type): pass

self.assertEqual(func1.__annotations__['a'], Union[int, sentinel])
self.assertEqual(func2.__annotations__['a'], Union[sentinel, int])
self.assertEqual(func1.__annotations__['a'], Union[int, sentinel_type])
self.assertEqual(func2.__annotations__['a'], Union[sentinel_type, int])

def test_sentinel_not_callable(self):
sentinel = Sentinel('sentinel')
sentinel_ = sentinel('sentinel')
with self.assertRaisesRegex(
TypeError,
"'Sentinel' object is not callable"
"'sentinel' object is not callable"
):
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_deprecated(self):
with self.assertWarnsRegex(DeprecationWarning, r"Subclassing sentinel is forbidden by PEP 661"):
class SentinelSubclass(Sentinel):
pass
with self.assertRaisesRegex(TypeError, r"First parameter 'name' is required"):
sentinel()

def test_sentinel_not_picklable(self):
sentinel = Sentinel('sentinel')
with self.assertRaisesRegex(
TypeError,
"Cannot pickle 'Sentinel' object"
):
pickle.dumps(sentinel)
with self.assertWarnsRegex(DeprecationWarning, r"'name' is positional-only and must not be a keyword parameter"):
my_sentinel = Sentinel(name="my_sentinel")
with self.assertWarnsRegex(DeprecationWarning, r"Setting attributes on sentinel is deprecated"):
my_sentinel.foo = "bar"


def load_tests(loader, tests, pattern):
import doctest
Expand Down
91 changes: 68 additions & 23 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
'overload',
'override',
'Protocol',
'sentinel',
'Sentinel',
'reveal_type',
'runtime',
Expand Down Expand Up @@ -159,22 +160,75 @@
# 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:

def _caller(depth=1, default='__main__'):
try:
return sys._getframemodulename(depth + 1) or default
except AttributeError: # For platforms without _getframemodulename()
pass
try:
return sys._getframe(depth + 1).f_globals.get('__name__', default)
except (AttributeError, ValueError): # For platforms without _getframe()
pass
return None


# Placeholder for sentinel methods, because sentinels can not have their own sentinels
_sentinel_placeholder = object()


class sentinel:
Comment thread
HexDecimal marked this conversation as resolved.
Outdated
"""Create a unique sentinel object.

*name* should be the name of the variable to which the return value shall be assigned.

*repr*, if supplied, will be used for the repr of the sentinel object.
If not provided, "<name>" will be used.
"""

def __init__(
self,
name: str,
__name: str = _sentinel_placeholder,
/,
repr: typing.Optional[str] = None,
):
self._name = name
self._repr = repr if repr is not None else f'<{name}>'
*,
name: str = _sentinel_placeholder,
) -> None:
if name is not _sentinel_placeholder:
warnings.warn(
"'name' is positional-only and must not be a keyword parameter",
Comment thread
HexDecimal marked this conversation as resolved.
Outdated
DeprecationWarning,
stacklevel=2,
)
__name = name
if __name is _sentinel_placeholder:
raise TypeError("First parameter 'name' is required")
if repr is not None:
warnings.warn(
"'repr' is deprecated and must be removed",
Comment thread
HexDecimal marked this conversation as resolved.
Outdated
DeprecationWarning,
stacklevel=2,
)

self.__name__ = __name
self._repr = repr if repr is not None else __name

# For pickling as a singleton:
self.__module__ = _caller()

def __init_subclass__(cls):
warnings.warn(
"Subclassing sentinel is forbidden by PEP 661",
Comment thread
HexDecimal marked this conversation as resolved.
Outdated
DeprecationWarning,
stacklevel=2,
)
super().__init_subclass__()

def __setattr__(self, attr: str, value: object) -> None:
if attr not in {"__name__", "_repr", "__module__"}:
warnings.warn(
"Setting attributes on sentinel is deprecated",
Comment thread
HexDecimal marked this conversation as resolved.
Outdated
DeprecationWarning,
stacklevel=2,
)
super().__setattr__(attr, value)

def __repr__(self):
return self._repr
Expand All @@ -193,11 +247,14 @@ 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 taken from the __module__ attribute

Sentinel = sentinel

_marker = sentinel("sentinel")

_marker = Sentinel("sentinel")

# The functions below are modified copies of typing internal helpers.
# They are needed by _ProtocolMeta and they provide support for PEP 646.
Expand Down Expand Up @@ -638,18 +695,6 @@ def _get_protocol_attrs(cls):
return attrs


def _caller(depth=1, default='__main__'):
try:
return sys._getframemodulename(depth + 1) or default
except AttributeError: # For platforms without _getframemodulename()
pass
try:
return sys._getframe(depth + 1).f_globals.get('__name__', default)
except (AttributeError, ValueError): # For platforms without _getframe()
pass
return None


# `__match_args__` attribute was removed from protocol members in 3.13,
# we want to backport this change to older Python versions.
# Breakpoint: https://github.com/python/cpython/pull/110683
Expand Down
Loading