Skip to content
Open
21 changes: 16 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 is deprecated.

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,18 @@ Sentinel objects

See :pep:`661`

.. versionchanged:: 4.16.0

The implementation of this class has been updated to conform to
the accepted version of :pep:`661`.

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.
Subclassing and attribute assignment are deprecated.


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' parameter is deprecated and will 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 deprecated"):
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"Passing 'name' as a keyword argument is deprecated"):
my_sentinel = Sentinel(name="my_sentinel")
with self.assertWarnsRegex(DeprecationWarning, r"Setting attribute 'foo' on sentinel objects is deprecated"):
my_sentinel.foo = "bar"


def load_tests(loader, tests, pattern):
import doctest
Expand Down
134 changes: 93 additions & 41 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,45 +160,108 @@
# 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:
"""Create a unique sentinel object.

*name* should be the name of the variable to which the return value shall be assigned.
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

*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,
repr: typing.Optional[str] = None,
):
self._name = name
self._repr = repr if repr is not None else f'<{name}>'
# Placeholder for sentinel methods, because sentinels can not have their own sentinels
_sentinel_placeholder = object()

def __repr__(self):
return self._repr
if hasattr(builtins, "sentinel"): # 3.15+
sentinel = builtins.sentinel
else:
class sentinel:
"""Create a unique sentinel object.

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")
*name* should be the name of the variable to which the return value
shall be assigned.
"""

def __init__(
self,
__name: str = _sentinel_placeholder,
/,
repr: typing.Optional[str] = None,
*,
name: str = _sentinel_placeholder,
) -> None:
if name is not _sentinel_placeholder:
warnings.warn(
"Passing 'name' as a keyword argument is deprecated; "
"pass it positionally instead.",
DeprecationWarning,
stacklevel=2,
)
__name = name
if __name is _sentinel_placeholder:
raise TypeError("First parameter 'name' is required")
if repr is not None:
warnings.warn(
"The 'repr' parameter is deprecated "
"and will be removed in Python 3.15.",
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 deprecated "
"and will be disallowed in Python 3.15",
DeprecationWarning,
stacklevel=2,
)
super().__init_subclass__()

def __setattr__(self, attr: str, value: object) -> None:
if attr not in {"__name__", "_repr", "__module__"}:
warnings.warn(
f"Setting attribute {attr!r} on sentinel objects is deprecated "
"and will be disallowed in Python 3.15.",
DeprecationWarning,
stacklevel=2,
)
super().__setattr__(attr, value)

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]

# 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 __ror__(self, other):
return typing.Union[other, self]
def __reduce__(self) -> str:
"""Reduce this sentinel to a singleton."""
return self.__name__ # Module is taken from the __module__ attribute

def __getstate__(self):
raise TypeError(f"Cannot pickle {type(self).__name__!r} object")
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 +702,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