Skip to content

Commit 730ce66

Browse files
committed
gh-148829: Make sentinels' repr and module customizable
Implementation of python/peps#4968; still needs SC approval.
1 parent c6fd7de commit 730ce66

14 files changed

Lines changed: 136 additions & 31 deletions

File tree

Doc/c-api/sentinel.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ Sentinel objects
1919
2020
.. versionadded:: 3.15
2121
22-
.. c:function:: PyObject* PySentinel_New(const char *name, const char *module_name)
22+
.. c:function:: PyObject* PySentinel_New(const char *name, const char *module_name, const char *repr)
2323
2424
Return a new :class:`sentinel` object with :attr:`~sentinel.__name__` set to
2525
*name* and :attr:`~sentinel.__module__` set to *module_name*.
2626
*name* must not be ``NULL``. If *module_name* is ``NULL``, :attr:`~sentinel.__module__`
27-
is set to ``None``.
27+
is set to ``None``. If *repr* is ``NULL``, ``repr()`` returns :attr:`~sentinel.__name__`.
2828
Return ``NULL`` with an exception set on failure.
2929
3030
For pickling to work, *module_name* must be the name of an importable

Doc/data/refcounts.dat

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2040,6 +2040,7 @@ PySeqIter_New:PyObject*:seq:0:
20402040
PySentinel_New:PyObject*::+1:
20412041
PySentinel_New:const char*:name::
20422042
PySentinel_New:const char*:module_name::
2043+
PySentinel_New:const char*:repr::
20432044

20442045
PySequence_Check:int:::
20452046
PySequence_Check:PyObject*:o:0:

Doc/library/functions.rst

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1827,15 +1827,21 @@ are always available. They are listed here in alphabetical order.
18271827
:func:`setattr`.
18281828

18291829

1830-
.. class:: sentinel(name, /)
1830+
.. class:: sentinel(name, /, *, repr=None)
18311831

18321832
Return a new unique sentinel object. *name* must be a :class:`str`, and is
1833-
used as the returned object's representation::
1833+
used by default as the returned object's representation::
18341834

18351835
>>> MISSING = sentinel("MISSING")
18361836
>>> MISSING
18371837
MISSING
18381838

1839+
The optional *repr* argument can be used to specify a different representation::
1840+
1841+
>>> MISSING = sentinel("MISSING", repr="<MISSING>")
1842+
>>> MISSING
1843+
<MISSING>
1844+
18391845
Sentinel objects are truthy and compare equal only to themselves. They are
18401846
intended to be compared with the :keyword:`is` operator.
18411847

@@ -1879,7 +1885,7 @@ are always available. They are listed here in alphabetical order.
18791885

18801886
.. attribute:: __module__
18811887

1882-
The name of the module where the sentinel was created.
1888+
The name of the module where the sentinel was created. This attribute is writable.
18831889

18841890
.. versionadded:: 3.15
18851891

Include/cpython/sentinelobject.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ PyAPI_DATA(PyTypeObject) PySentinel_Type;
1313

1414
PyAPI_FUNC(PyObject *) PySentinel_New(
1515
const char *name,
16-
const char *module_name);
16+
const char *module_name,
17+
const char *repr);
1718

1819
#ifdef __cplusplus
1920
}

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,7 @@ struct _Py_global_strings {
754754
STRUCT_FOR_ID(repeat)
755755
STRUCT_FOR_ID(repl)
756756
STRUCT_FOR_ID(replace)
757+
STRUCT_FOR_ID(repr)
757758
STRUCT_FOR_ID(reqrefs)
758759
STRUCT_FOR_ID(require_ready)
759760
STRUCT_FOR_ID(reserved)

Include/internal/pycore_runtime_init_generated.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_builtin.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1956,16 +1956,33 @@ def test_sentinel(self):
19561956
with self.assertRaises(TypeError):
19571957
class SubSentinel(sentinel):
19581958
pass
1959+
1960+
def test_sentinel_attributes(self):
1961+
missing = sentinel("MISSING")
19591962
with self.assertRaises(TypeError):
19601963
sentinel.attribute = "value"
19611964
with self.assertRaises(AttributeError):
1962-
missing.__name__ = "CHANGED"
1965+
missing.attribute = "value"
19631966
with self.assertRaises(AttributeError):
1964-
missing.__module__ = "changed"
1967+
missing.__name__ = "CHANGED"
1968+
missing.__module__ = "changed"
1969+
self.assertEqual(missing.__module__, "changed")
19651970
with self.assertRaises(AttributeError):
19661971
del missing.__name__
1972+
del missing.__module__
19671973
with self.assertRaises(AttributeError):
1968-
del missing.__module__
1974+
missing.__module__
1975+
1976+
def test_sentinel_repr(self):
1977+
with_repr = sentinel("WITH_REPR", repr="custom")
1978+
without_repr = sentinel("WITHOUT_REPR", repr=None)
1979+
self.assertEqual(repr(with_repr), "custom")
1980+
self.assertEqual(repr(without_repr), "WITHOUT_REPR")
1981+
self.assertEqual(str(with_repr), "custom")
1982+
self.assertEqual(str(without_repr), "WITHOUT_REPR")
1983+
1984+
with self.assertRaisesRegex(TypeError, "repr.*str or None"):
1985+
sentinel("BAD_REPR", repr=42)
19691986

19701987
def test_sentinel_pickle(self):
19711988
for proto in range(pickle.HIGHEST_PROTOCOL + 1):

Lib/test/test_capi/test_object.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ def test_pysentinel_new(self):
8080
self.assertEqual(no_module.__name__, "NO_MODULE")
8181
self.assertIs(no_module.__module__, None)
8282

83+
with_repr = _testcapi.pysentinel_new("WITH_REPR", __name__, "custom repr")
84+
self.assertIs(type(with_repr), sentinel)
85+
self.assertEqual(with_repr.__name__, "WITH_REPR")
86+
self.assertEqual(with_repr.__module__, __name__)
87+
self.assertEqual(repr(with_repr), "custom repr")
88+
8389
globals()["CAPI_SENTINEL"] = marker
8490
self.addCleanup(globals().pop, "CAPI_SENTINEL", None)
8591
self.assertIs(pickle.loads(pickle.dumps(marker)), marker)

0 commit comments

Comments
 (0)