Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Add support for sentinels (PEP 661) #594

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 13, 2025
Merged
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
2 changes: 2 additions & 0 deletions 2 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ New features:
Patch by [Victorien Plot](https://github.com/Viicos).
- Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by
Sebastian Rittau.
- Add support for sentinels ([PEP 661](https://peps.python.org/pep-0661/)). Patch by
[Victorien Plot](https://github.com/Viicos).

# Release 4.13.2 (April 10, 2025)

Expand Down
28 changes: 28 additions & 0 deletions 28 doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,34 @@ Capsule objects
.. versionadded:: 4.12.0


Sentinel objects
~~~~~~~~~~~~~~~~

.. class:: Sentinel(name, 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.

If *repr* is provided, it will be used for the :meth:`~object.__repr__`
of the sentinel object. If not provided, ``"<name>"`` will be used.

Example::

>>> 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)
... else:
... assert_type(arg, int)
...
>>> func(MISSING)

.. versionadded:: 4.14.0

See :pep:`661`


Pure aliases
~~~~~~~~~~~~

Expand Down
40 changes: 40 additions & 0 deletions 40 src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
ReadOnly,
Required,
Self,
Sentinel,
Set,
Tuple,
Type,
Expand Down Expand Up @@ -9096,5 +9097,44 @@ 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>')

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()
37 changes: 37 additions & 0 deletions 37 src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
'overload',
'override',
'Protocol',
'Sentinel',
'reveal_type',
'runtime',
'runtime_checkable',
Expand Down Expand Up @@ -4210,6 +4211,42 @@ def evaluate_forward_ref(
)


class Sentinel:
"""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,
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")

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


# Aliases for items that are in typing in all supported versions.
# We use hasattr() checks so this library will continue to import on
# future versions of Python that may remove these names.
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.