diff --git a/doc/index.rst b/doc/index.rst index 66577ef0..49f2c170 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1071,18 +1071,17 @@ Capsule objects Sentinel objects ~~~~~~~~~~~~~~~~ -.. class:: Sentinel(name, repr=None) +.. class:: sentinel(name, /) 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. + 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) @@ -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. + 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 ~~~~~~~~~~~~ diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index f07e1eb0..0ba95fc4 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -102,6 +102,7 @@ reveal_type, runtime, runtime_checkable, + sentinel, type_repr, ) @@ -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), '') + 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 diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 20c331ee..71b7954d 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -91,6 +91,7 @@ 'overload', 'override', 'Protocol', + 'sentinel', 'Sentinel', 'reveal_type', 'runtime', @@ -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, "" 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. @@ -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