Skip to content
Draft
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
4 changes: 2 additions & 2 deletions Doc/c-api/sentinel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ Sentinel objects

.. versionadded:: 3.15

.. c:function:: PyObject* PySentinel_New(const char *name, const char *module_name)
.. c:function:: PyObject* PySentinel_New(const char *name, const char *module_name, const char *repr)

Return a new :class:`sentinel` object with :attr:`~sentinel.__name__` set to
*name* and :attr:`~sentinel.__module__` set to *module_name*.
*name* must not be ``NULL``. If *module_name* is ``NULL``, :attr:`~sentinel.__module__`
is set to ``None``.
is set to ``None``. If *repr* is ``NULL``, ``repr()`` returns :attr:`~sentinel.__name__`.
Return ``NULL`` with an exception set on failure.

For pickling to work, *module_name* must be the name of an importable
Expand Down
1 change: 1 addition & 0 deletions Doc/data/refcounts.dat
Original file line number Diff line number Diff line change
Expand Up @@ -2040,6 +2040,7 @@ PySeqIter_New:PyObject*:seq:0:
PySentinel_New:PyObject*::+1:
PySentinel_New:const char*:name::
PySentinel_New:const char*:module_name::
PySentinel_New:const char*:repr::

PySequence_Check:int:::
PySequence_Check:PyObject*:o:0:
Expand Down
12 changes: 9 additions & 3 deletions Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1827,15 +1827,21 @@ are always available. They are listed here in alphabetical order.
:func:`setattr`.


.. class:: sentinel(name, /)
.. class:: sentinel(name, /, *, repr=None)

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

>>> MISSING = sentinel("MISSING")
>>> MISSING
MISSING

The optional *repr* argument can be used to specify a different representation::

>>> MISSING = sentinel("MISSING", repr="<MISSING>")
>>> MISSING
<MISSING>

Sentinel objects are truthy and compare equal only to themselves. They are
intended to be compared with the :keyword:`is` operator.

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

.. attribute:: __module__

The name of the module where the sentinel was created.
The name of the module where the sentinel was created. This attribute is writable.

.. versionadded:: 3.15

Expand Down
3 changes: 2 additions & 1 deletion Include/cpython/sentinelobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ PyAPI_DATA(PyTypeObject) PySentinel_Type;

PyAPI_FUNC(PyObject *) PySentinel_New(
const char *name,
const char *module_name);
const char *module_name,
const char *repr);

#ifdef __cplusplus
}
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(repeat)
STRUCT_FOR_ID(repl)
STRUCT_FOR_ID(replace)
STRUCT_FOR_ID(repr)
STRUCT_FOR_ID(reqrefs)
STRUCT_FOR_ID(require_ready)
STRUCT_FOR_ID(reserved)
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 20 additions & 3 deletions Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1956,16 +1956,33 @@ def test_sentinel(self):
with self.assertRaises(TypeError):
class SubSentinel(sentinel):
pass

def test_sentinel_attributes(self):
missing = sentinel("MISSING")
with self.assertRaises(TypeError):
sentinel.attribute = "value"
with self.assertRaises(AttributeError):
missing.__name__ = "CHANGED"
missing.attribute = "value"
with self.assertRaises(AttributeError):
missing.__module__ = "changed"
missing.__name__ = "CHANGED"
missing.__module__ = "changed"
self.assertEqual(missing.__module__, "changed")
with self.assertRaises(AttributeError):
del missing.__name__
del missing.__module__
with self.assertRaises(AttributeError):
del missing.__module__
missing.__module__

def test_sentinel_repr(self):
with_repr = sentinel("WITH_REPR", repr="custom")
without_repr = sentinel("WITHOUT_REPR", repr=None)
self.assertEqual(repr(with_repr), "custom")
self.assertEqual(repr(without_repr), "WITHOUT_REPR")
self.assertEqual(str(with_repr), "custom")
self.assertEqual(str(without_repr), "WITHOUT_REPR")

with self.assertRaisesRegex(TypeError, "repr.*str or None"):
sentinel("BAD_REPR", repr=42)

def test_sentinel_pickle(self):
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/test_capi/test_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ def test_pysentinel_new(self):
self.assertEqual(no_module.__name__, "NO_MODULE")
self.assertIs(no_module.__module__, None)

with_repr = _testcapi.pysentinel_new("WITH_REPR", __name__, "custom repr")
self.assertIs(type(with_repr), sentinel)
self.assertEqual(with_repr.__name__, "WITH_REPR")
self.assertEqual(with_repr.__module__, __name__)
self.assertEqual(repr(with_repr), "custom repr")

globals()["CAPI_SENTINEL"] = marker
self.addCleanup(globals().pop, "CAPI_SENTINEL", None)
self.assertIs(pickle.loads(pickle.dumps(marker)), marker)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:class:`sentinel` objects now support a ``repr=`` argument and their
:attr:`~sentinel.__module__` attribute is writable.
5 changes: 3 additions & 2 deletions Modules/_testcapi/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -560,10 +560,11 @@ pysentinel_new(PyObject *self, PyObject *args)
{
const char *name;
const char *module_name = NULL;
if (!PyArg_ParseTuple(args, "s|s", &name, &module_name)) {
const char *repr = NULL;
if (!PyArg_ParseTuple(args, "s|ss", &name, &module_name, &repr)) {
return NULL;
}
return PySentinel_New(name, module_name);
return PySentinel_New(name, module_name, repr);
}

static PyObject *
Expand Down
62 changes: 50 additions & 12 deletions Objects/clinic/sentinelobject.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 34 additions & 8 deletions Objects/sentinelobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ typedef struct {
PyObject_HEAD
PyObject *name;
PyObject *module;
PyObject *repr;
} sentinelobject;

#define sentinelobject_CAST(op) \
Expand Down Expand Up @@ -46,7 +47,7 @@ caller(void)
}

static PyObject *
sentinel_new_with_module(PyTypeObject *type, PyObject *name, PyObject *module)
sentinel_new_with_module(PyTypeObject *type, PyObject *name, PyObject *module, PyObject *repr)
{
assert(PyUnicode_Check(name));

Expand All @@ -56,6 +57,7 @@ sentinel_new_with_module(PyTypeObject *type, PyObject *name, PyObject *module)
}
self->name = Py_NewRef(name);
self->module = Py_NewRef(module);
self->repr = Py_XNewRef(repr);
_PyObject_GC_TRACK(self);
return (PyObject *)self;
}
Expand All @@ -66,37 +68,56 @@ sentinel.__new__ as sentinel_new

name: object(subclass_of='&PyUnicode_Type')
/
*
repr: object = None
[clinic start generated code]*/

static PyObject *
sentinel_new_impl(PyTypeObject *type, PyObject *name)
/*[clinic end generated code: output=4af55c6048bed30d input=3ab75704f39c119c]*/
sentinel_new_impl(PyTypeObject *type, PyObject *name, PyObject *repr)
/*[clinic end generated code: output=1eb7fab52e57d8c8 input=28cab6c468997b35]*/
{
if (repr == Py_None) {
repr = NULL;
}
else if (!PyUnicode_Check(repr)) {
_PyArg_BadArgument("sentinel", "argument 'repr'", "str or None", repr);
return NULL;
}
PyObject *module = caller();
PyObject *self = sentinel_new_with_module(type, name, module);
PyObject *self = sentinel_new_with_module(type, name, module, repr);
Py_DECREF(module);
return self;
}

PyObject *
PySentinel_New(const char *name, const char *module_name)
PySentinel_New(const char *name, const char *module_name, const char *repr)
{
PyObject *name_obj = PyUnicode_FromString(name);
if (name_obj == NULL) {
return NULL;
}
PyObject *repr_obj = NULL;
if (repr != NULL) {
repr_obj = PyUnicode_FromString(repr);
if (repr_obj == NULL) {
Py_DECREF(name_obj);
return NULL;
}
}
PyObject *module_obj = module_name == NULL
? Py_None
: PyUnicode_FromString(module_name);
if (module_obj == NULL) {
Py_DECREF(name_obj);
Py_XDECREF(repr_obj);
return NULL;
}

PyObject *sentinel = sentinel_new_with_module(
&PySentinel_Type, name_obj, module_obj);
&PySentinel_Type, name_obj, module_obj, repr_obj);
Py_DECREF(module_obj);
Py_DECREF(name_obj);
Py_XDECREF(repr_obj);
return sentinel;
}

Expand All @@ -106,6 +127,7 @@ sentinel_clear(PyObject *op)
sentinelobject *self = sentinelobject_CAST(op);
Py_CLEAR(self->name);
Py_CLEAR(self->module);
Py_CLEAR(self->repr);
return 0;
}

Expand All @@ -123,13 +145,17 @@ sentinel_traverse(PyObject *op, visitproc visit, void *arg)
sentinelobject *self = sentinelobject_CAST(op);
Py_VISIT(self->name);
Py_VISIT(self->module);
Py_VISIT(self->repr);
return 0;
}

static PyObject *
sentinel_repr(PyObject *op)
{
sentinelobject *self = sentinelobject_CAST(op);
if (self->repr != NULL) {
return Py_NewRef(self->repr);
}
return Py_NewRef(self->name);
}

Expand Down Expand Up @@ -161,7 +187,7 @@ static PyMethodDef sentinel_methods[] = {

static PyMemberDef sentinel_members[] = {
{"__name__", Py_T_OBJECT_EX, offsetof(sentinelobject, name), Py_READONLY},
{"__module__", Py_T_OBJECT_EX, offsetof(sentinelobject, module), Py_READONLY},
{"__module__", Py_T_OBJECT_EX, offsetof(sentinelobject, module), 0},
{NULL}
};

Expand All @@ -170,7 +196,7 @@ static PyNumberMethods sentinel_as_number = {
};

PyDoc_STRVAR(sentinel_doc,
"sentinel(name, /)\n"
"sentinel(name, /, *, repr=None)\n"
"--\n\n"
"Create a unique sentinel object with the given name.");

Expand Down
Loading