Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ New features:
- Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by
Sebastian Rittau.
- Fix tests for Python 3.14. Patch by Jelle Zijlstra.
- Add support for sentinels ([PEP 661](https://peps.python.org/pep-0661/)).

# Release 4.13.2 (April 10, 2025)

Expand Down
45 changes: 45 additions & 0 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
ReadOnly,
Required,
Self,
Sentinel,
Set,
Tuple,
Type,
Expand Down Expand Up @@ -9088,5 +9089,49 @@ def test_invalid_special_forms(self):
self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)), ClassVar)


class TestSentinels(BaseTestCase):
def test_sentinel_no_repr(self):
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>')

sentinel_no_repr_dots = Sentinel('Test.sentinel_no_repr')

self.assertEqual(sentinel_no_repr_dots._name, 'Test.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')

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')

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

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

def test_sentinel_not_callable(self):
sentinel = Sentinel('sentinel')
with self.assertRaisesRegex(
TypeError,
"'Sentinel' object is not callable"
):
sentinel()

def test_sentinel_not_picklable(self):
sentinel = Sentinel('sentinel')
with self.assertRaisesRegex(
TypeError,
"Cannot pickle 'Sentinel' object"
):
pickle.dumps(sentinel)


if __name__ == '__main__':
main()
40 changes: 40 additions & 0 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pyright: ignore
import abc
import builtins
import collections
Expand Down Expand Up @@ -89,6 +90,7 @@
'overload',
'override',
'Protocol',
'Sentinel',
'reveal_type',
'runtime',
'runtime_checkable',
Expand Down Expand Up @@ -4222,6 +4224,44 @@ def evaluate_forward_ref(
)


class Sentinel:
"""Create a unique sentinel object.

*name* should be the fully-qualified name of the variable to which the
Copy link
Member

Choose a reason for hiding this comment

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

Fully-qualified? That doesn't match the PEP which does MISSING = Sentinel("MISSING").

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was copied verbatim from https://github.com/taleinat/python-stdlib-sentinels/, updated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually the repr construction by splitting dots don't make sense anymore, so I also updated the logic in the last logic.

Choose a reason for hiding this comment

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

It'd make sense to be fully qualified inside the module, so Sentinel("SomeClass.CONST"). Pickle expects that if we decide to use it. It's recommended in the PEP under "Additional Notes", but not required.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It'd make sense to be fully qualified inside the module, so Sentinel("SomeClass.CONST"). Pickle expects that if we decide to use it. It's recommended in the PEP under "Additional Notes", but not required.

This will have to be decided in the PEP discussion.

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 (with any leading class names
removed).
"""

def __init__(
self,
name: str,
repr: typing.Optional[str] = None,
):
self._name = name
self._repr = repr if repr is not None else f'<{name.split(".")[-1]}>'

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")

def __or__(self, other):
return Union[self, other]

def __ror__(self, other):
return Union[other, self]

def __getstate__(self):
raise TypeError(f"Cannot pickle {type(self).__name__!r} object")


# Aliases for items that are in typing in all supported versions.
# Explicitly assign these (rather than using `from typing import *` at the top),
# so that we get a CI error if one of these is deleted from typing.py
Expand Down
Loading