Skip to content

Commit 9dc25bc

Browse files
committed
Implement sentinel objects as types
Adds support for isinstance and match statements Methods required to make sentinels pretend they are types are no longer necessary `self._name` replaced with `self.__name__` Updated tests and documentation
1 parent 9d18f86 commit 9dc25bc

File tree

3 files changed

+60
-33
lines changed

3 files changed

+60
-33
lines changed

doc/index.rst

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,17 +1067,28 @@ Sentinel objects
10671067
If *repr* is provided, it will be used for the :meth:`~object.__repr__`
10681068
of the sentinel object. If not provided, ``"<name>"`` will be used.
10691069

1070+
Sentinels can be tested using :ref:`is`, :func:`isinstance`,
1071+
or :ref:`match`.
1072+
10701073
Example::
10711074

10721075
>>> from typing_extensions import Sentinel, assert_type
10731076
>>> MISSING = Sentinel('MISSING')
1074-
>>> def func(arg: int | MISSING = MISSING) -> None:
1077+
>>> def check_identity(arg: int | MISSING = MISSING) -> None:
10751078
... if arg is MISSING:
10761079
... assert_type(arg, MISSING)
10771080
... else:
10781081
... assert_type(arg, int)
10791082
...
1080-
>>> func(MISSING)
1083+
>>> check_identity(MISSING)
1084+
>>> def check_match(arg: int | MISSING = MISSING) -> None:
1085+
... match arg:
1086+
... case MISSING():
1087+
... assert_type(arg, MISSING)
1088+
... case int()
1089+
... assert_type(arg, int)
1090+
...
1091+
>>> check_match(MISSING)
10811092

10821093
Sentinels defined inside a class scope should use a :term:`qualified name`.
10831094

src/test_typing_extensions.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9546,9 +9546,11 @@ class TestSentinels(BaseTestCase):
95469546
def test_sentinel_no_repr(self):
95479547
sentinel_no_repr = Sentinel('sentinel_no_repr')
95489548

9549-
self.assertEqual(sentinel_no_repr._name, 'sentinel_no_repr')
9549+
self.assertEqual(sentinel_no_repr.__name__, 'sentinel_no_repr')
95509550
self.assertEqual(repr(sentinel_no_repr), '<sentinel_no_repr>')
95519551

9552+
self.assertEqual(repr(Sentinel), "<class 'typing_extensions.Sentinel'>")
9553+
95529554
def test_sentinel_explicit_repr(self):
95539555
sentinel_explicit_repr = Sentinel('sentinel_explicit_repr', repr='explicit_repr')
95549556

@@ -9568,7 +9570,7 @@ def test_sentinel_not_callable(self):
95689570
sentinel = Sentinel('sentinel')
95699571
with self.assertRaisesRegex(
95709572
TypeError,
9571-
"'Sentinel' object is not callable"
9573+
f"Sentinel object {re.escape(repr(sentinel))} is not callable"
95729574
):
95739575
sentinel()
95749576

@@ -9593,6 +9595,21 @@ def test_sentinel_picklable_anonymous(self):
95939595
):
95949596
self.assertIs(anonymous_sentinel, pickle.loads(pickle.dumps(anonymous_sentinel, protocol=proto)))
95959597

9598+
def test_sentinel_isinstance(self):
9599+
anonymous_sentinel = Sentinel("anonymous_sentinel")
9600+
self.assertIsInstance(self.SENTINEL, self.SENTINEL)
9601+
self.assertIsInstance(anonymous_sentinel, anonymous_sentinel)
9602+
self.assertNotIsInstance(self.SENTINEL, anonymous_sentinel)
9603+
9604+
self.assertIsInstance(self.SENTINEL, object)
9605+
self.assertIsInstance(self.SENTINEL, type)
9606+
self.assertNotIsInstance(self.SENTINEL, Sentinel)
9607+
9608+
self.assertIsSubclass(self.SENTINEL, object)
9609+
self.assertIsSubclass(self.SENTINEL, Sentinel)
9610+
self.assertIsSubclass(self.SENTINEL, self.SENTINEL)
9611+
self.assertNotIsSubclass(self.SENTINEL, anonymous_sentinel)
9612+
95969613
def load_tests(loader, tests, pattern):
95979614
import doctest
95989615
tests.addTests(doctest.DocTestSuite(typing_extensions))

src/typing_extensions.py

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,17 @@
159159
# Added with bpo-45166 to 3.10.1+ and some 3.9 versions
160160
_FORWARD_REF_HAS_CLASS = "__forward_is_class__" in typing.ForwardRef.__slots__
161161

162-
class Sentinel:
162+
class _SentinelMeta(type):
163+
def __instancecheck__(self, instance) -> bool:
164+
return self is instance
165+
166+
def __repr__(self) -> str:
167+
if self is Sentinel:
168+
return super().__repr__() # self._repr missing on base class
169+
return self._repr
170+
171+
172+
class Sentinel(metaclass=_SentinelMeta):
163173
"""Create a unique sentinel object.
164174
165175
*name* should be the name of the variable to which the return value shall be assigned.
@@ -171,39 +181,28 @@ class Sentinel:
171181
If not provided, "<name>" will be used.
172182
"""
173183

174-
def __init__(
175-
self,
184+
def __new__(
185+
cls,
176186
name: str,
177187
module_name: typing.Optional[str] = None,
178188
*,
179189
repr: typing.Optional[str] = None,
180190
):
181-
self._name = name
182-
self._repr = repr if repr is not None else f'<{name}>'
183-
184-
# For pickling as a singleton:
185-
self.__module__ = module_name if module_name is not None else _caller()
186-
187-
def __repr__(self):
188-
return self._repr
189-
190-
if sys.version_info < (3, 11):
191-
# The presence of this method convinces typing._type_check
192-
# that Sentinels are types.
193-
def __call__(self, *args, **kwargs):
194-
raise TypeError(f"{type(self).__name__!r} object is not callable")
195-
196-
# Breakpoint: https://github.com/python/cpython/pull/21515
197-
if sys.version_info >= (3, 10):
198-
def __or__(self, other):
199-
return typing.Union[self, other]
200-
201-
def __ror__(self, other):
202-
return typing.Union[other, self]
203-
204-
def __reduce__(self) -> str:
205-
"""Reduce this sentinel to a singleton."""
206-
return self._name # Module is taken from the __module__ attribute
191+
def stubbed_call(cls, *args, **kwargs):
192+
raise TypeError(f"Sentinel object {cls!r} is not callable")
193+
194+
repr = repr if repr is not None else f'<{name}>'
195+
module_name = module_name if module_name is not None else _caller()
196+
197+
return type(
198+
name,
199+
(cls,),
200+
{
201+
"__new__": stubbed_call, # Disable calling sentinel definitions
202+
"_repr": repr,
203+
"__module__": module_name, # For pickling
204+
},
205+
)
207206

208207

209208
_marker = Sentinel("sentinel", __name__)

0 commit comments

Comments
 (0)