From 7626068f99ea3c5ec922cd6b40fd7a6b35ea04aa Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Thu, 8 May 2025 17:29:22 +0200 Subject: [PATCH 1/7] Add support for sentinels (PEP 661) --- CHANGELOG.md | 1 + src/test_typing_extensions.py | 37 ++++++++++++++++++++++++++++ src/typing_extensions.py | 46 +++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ba8e152..6b1e37d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index dc882f9f..ab3f2470 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -66,6 +66,7 @@ ReadOnly, Required, Self, + Sentinel, Set, Tuple, Type, @@ -9088,5 +9089,41 @@ 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_dots = Sentinel('Test.sentinel_no_repr') + + self.assertEqual(sentinel_no_repr_dots._name, 'Test.sentinel_no_repr') + self.assertEqual(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() + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 269ca650..4c3c847b 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1,3 +1,4 @@ +# pyright: ignore import abc import builtins import collections @@ -89,6 +90,7 @@ 'overload', 'override', 'Protocol', + 'Sentinel', 'reveal_type', 'runtime', 'runtime_checkable', @@ -4222,6 +4224,50 @@ def evaluate_forward_ref( ) +class Sentinel: + """Create a unique sentinel object. + + *name* should be the fully-qualified 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, "" will be used (with any leading class names + removed). + """ + + def __init__( + self, + name: str, + repr: str | None = None, + ): + self._name = name + self._repr = repr if repr is not None else f'<{name.split(".")[-1]}>' + + def __repr__(self): + return self._repr + + def __reduce__(self): + return ( + type(self), + ( + self._name, + 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] + + # 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 From 2be531efc102781a3a720d0339dc58c0914acf56 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Fri, 9 May 2025 09:32:12 +0200 Subject: [PATCH 2/7] Remove __reduce__ --- src/typing_extensions.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 4c3c847b..c209d5aa 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -4246,15 +4246,6 @@ def __init__( def __repr__(self): return self._repr - def __reduce__(self): - return ( - type(self), - ( - self._name, - self._repr, - ) - ) - if sys.version_info < (3, 11): # The presence of this method convinces typing._type_check # that Sentinels are types. From eb0e089c2b8b8525df1a963983a87fd3e0ee553a Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Fri, 9 May 2025 10:45:38 +0200 Subject: [PATCH 3/7] Fix --- src/typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index c209d5aa..00844ecc 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -4238,7 +4238,7 @@ class Sentinel: def __init__( self, name: str, - repr: str | None = None, + repr: typing.Optional[str] = None, ): self._name = name self._repr = repr if repr is not None else f'<{name.split(".")[-1]}>' From 6333253c00e444dc223bedfbf8889e1b2e897980 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Fri, 9 May 2025 17:11:45 +0200 Subject: [PATCH 4/7] Disable pickling --- src/test_typing_extensions.py | 8 ++++++++ src/typing_extensions.py | 3 +++ 2 files changed, 11 insertions(+) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index ab3f2470..f197980a 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -9124,6 +9124,14 @@ def test_sentinel_not_callable(self): ): 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() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 00844ecc..26c5ed22 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -4258,6 +4258,9 @@ def __or__(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), From fe206a4449ee361da662778f34d0ef62d544fb61 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Fri, 9 May 2025 18:34:50 +0200 Subject: [PATCH 5/7] Feedback --- doc/index.rst | 28 ++++++++++++++++++++++++++++ src/test_typing_extensions.py | 5 ----- src/typing_extensions.py | 9 +++------ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 325182eb..f8e3a27b 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1017,6 +1017,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, ``""`` 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 ~~~~~~~~~~~~ diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index f197980a..f1cfd319 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -9096,11 +9096,6 @@ def test_sentinel_no_repr(self): self.assertEqual(sentinel_no_repr._name, 'sentinel_no_repr') self.assertEqual(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), '') - def test_sentinel_explicit_repr(self): sentinel_explicit_repr = Sentinel('sentinel_explicit_repr', repr='explicit_repr') diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 26c5ed22..4df92004 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1,4 +1,3 @@ -# pyright: ignore import abc import builtins import collections @@ -4227,12 +4226,10 @@ def evaluate_forward_ref( class Sentinel: """Create a unique sentinel object. - *name* should be the fully-qualified name of the variable to which the - return value shall be assigned. + *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, "" will be used (with any leading class names - removed). + If not provided, "" will be used. """ def __init__( @@ -4241,7 +4238,7 @@ def __init__( repr: typing.Optional[str] = None, ): self._name = name - self._repr = repr if repr is not None else f'<{name.split(".")[-1]}>' + self._repr = repr if repr is not None else f'<{name}>' def __repr__(self): return self._repr From a724058ee5b700004a1922eb2f095cbbfb10b720 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Tue, 13 May 2025 09:30:22 +0200 Subject: [PATCH 6/7] lint --- src/typing_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 120d4352..d4e92a4c 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -4238,10 +4238,10 @@ def __call__(self, *args, **kwargs): raise TypeError(f"{type(self).__name__!r} object is not callable") def __or__(self, other): - return Union[self, other] + return typing.Union[self, other] def __ror__(self, other): - return Union[other, self] + return typing.Union[other, self] def __getstate__(self): raise TypeError(f"Cannot pickle {type(self).__name__!r} object") From 03c18eb8750768237cc823844b4ce7ed3c810a37 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Tue, 13 May 2025 09:32:35 +0200 Subject: [PATCH 7/7] Update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e38e06..92a19a3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +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/)). +- 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) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy