Skip to content

Commit add7fdb

Browse files
committed
Add Sentinel repr keyword and ensure backwards compatibility
Adds tests for both the keyword and old positional repr parameters
1 parent f405793 commit add7fdb

File tree

3 files changed

+46
-3
lines changed

3 files changed

+46
-3
lines changed

doc/index.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1030,7 +1030,7 @@ Capsule objects
10301030
Sentinel objects
10311031
~~~~~~~~~~~~~~~~
10321032

1033-
.. class:: Sentinel(name, module_name=None)
1033+
.. class:: Sentinel(name, module_name=None, *, repr=None)
10341034

10351035
A type used to define custom sentinel values.
10361036

@@ -1040,6 +1040,10 @@ Sentinel objects
10401040
*module_name* is the module where the sentinel is defined.
10411041
Defaults to the current modules ``__name__``.
10421042

1043+
If *repr* is provided, it will be used for the :meth:`~object.__repr__`
1044+
of the sentinel object. If not provided, *name* will be used.
1045+
Only the initial definition of the sentinel can configure *repr*.
1046+
10431047
All sentinels with the same *name* and *module_name* have the same identity.
10441048
Sentinel objects are tested using :py:ref:`is`.
10451049
Sentinel identity is preserved across :py:mod:`copy` and :py:mod:`pickle`.

src/test_typing_extensions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9275,6 +9275,20 @@ def test_sentinel_repr(self):
92759275
self.assertEqual(repr(TestSentinels.SENTINEL), "TestSentinels.SENTINEL")
92769276
self.assertEqual(repr(Sentinel("sentinel")), "sentinel")
92779277

9278+
def test_sentinel_explicit_repr(self):
9279+
sentinel_explicit_repr = Sentinel("sentinel_explicit_repr", repr="explicit_repr")
9280+
self.assertEqual(repr(sentinel_explicit_repr), "explicit_repr")
9281+
self.assertEqual(repr(Sentinel("sentinel_explicit_repr")), "explicit_repr")
9282+
9283+
def test_sentinel_explicit_repr_deprecated(self):
9284+
with self.assertWarnsRegex(
9285+
DeprecationWarning,
9286+
r"Use keyword parameter repr='explicit_repr' instead"
9287+
):
9288+
deprecated_repr = Sentinel("deprecated_repr", "explicit_repr")
9289+
self.assertEqual(repr(deprecated_repr), "explicit_repr")
9290+
self.assertEqual(repr(Sentinel("deprecated_repr")), "explicit_repr")
9291+
92789292
@skipIf(sys.version_info < (3, 10), reason='New unions not available in 3.9')
92799293
def test_sentinel_type_expression_union(self):
92809294
sentinel = Sentinel('sentinel')

src/typing_extensions.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4168,6 +4168,10 @@ class Sentinel:
41684168
*module_name* is the module where the sentinel is defined.
41694169
Defaults to the current modules ``__name__``.
41704170
4171+
*repr*, if supplied, will be used for the repr of the sentinel object.
4172+
If not provided, *name* will be used.
4173+
Only the initial definition of the sentinel can configure *repr*.
4174+
41714175
All sentinels with the same *name* and *module_name* have the same identity.
41724176
The ``is`` operator is used to test if an object is a sentinel.
41734177
Sentinel identity is preserved across copy and pickle.
@@ -4177,8 +4181,27 @@ def __new__(
41774181
cls,
41784182
name: str,
41794183
module_name: typing.Optional[str] = None,
4184+
*,
4185+
repr: typing.Optional[str] = None,
41804186
):
41814187
"""Return an object with a consistent identity."""
4188+
if module_name is not None and repr is None:
4189+
# 'repr' used to be the 2nd positional argument but is now 'module_name'
4190+
# Test if 'module_name' is a module or is the old 'repr' argument
4191+
# Use 'repr=name' to suppress this check
4192+
try:
4193+
importlib.import_module(module_name)
4194+
except Exception:
4195+
repr = module_name
4196+
module_name = None
4197+
warnings.warn(
4198+
"'repr' as a positional argument could be mistaken for the sentinels"
4199+
" 'module_name'."
4200+
f" Use keyword parameter repr={repr!r} instead.",
4201+
category=DeprecationWarning,
4202+
stacklevel=2,
4203+
)
4204+
41824205
if module_name is None:
41834206
module_name = _caller(default="")
41844207

@@ -4198,6 +4221,7 @@ def __new__(
41984221
sentinel = super().__new__(cls)
41994222
sentinel._name = name
42004223
sentinel._module_name = module_name
4224+
sentinel._repr = repr if repr is not None else name
42014225
return _sentinel_registry.setdefault(registry_key, sentinel)
42024226

42034227
@staticmethod
@@ -4214,7 +4238,7 @@ def _import_sentinel(name: str, module_name: str):
42144238
return None
42154239

42164240
def __repr__(self) -> str:
4217-
return self._name
4241+
return self._repr
42184242

42194243
if sys.version_info < (3, 11):
42204244
# The presence of this method convinces typing._type_check
@@ -4232,7 +4256,8 @@ def __ror__(self, other):
42324256
@classmethod
42334257
def _unpickle_fetch_sentinel(cls, name: str, module_name: str):
42344258
"""Unpickle using the sentinels location."""
4235-
return cls(name, module_name)
4259+
# Explicit repr=name because a saved module_name is known to be valid
4260+
return cls(name, module_name, repr=name)
42364261

42374262
def __reduce__(self):
42384263
"""Record where this sentinel is defined."""

0 commit comments

Comments
 (0)