Skip to content
Open
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
1 change: 1 addition & 0 deletions Doc/c-api/concrete.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ Other Objects
picklebuffer.rst
weakref.rst
capsule.rst
sentinel.rst
frame.rst
gen.rst
coro.rst
Expand Down
31 changes: 31 additions & 0 deletions Doc/c-api/sentinel.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.. highlight:: c

.. _sentinelobjects:

Sentinel Objects
----------------

.. c:var:: PyTypeObject PySentinel_Type

This instance of :c:type:`PyTypeObject` represents the Python
:class:`sentinel` type. This is the same object as :class:`sentinel`.

.. versionadded:: next

.. c:function:: int PySentinel_Check(PyObject *o)

Return true if *o* is a :class:`sentinel` object. The :class:`sentinel` type
does not allow subclasses, so this check is exact.

.. versionadded:: next

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

Return a new :class:`sentinel` object with :attr:`~sentinel.__name__` set to
*name* and :attr:`~sentinel.__module__` set to *module_name*.
Return ``NULL`` with an exception set on failure.

*module_name* should be the name of an importable module if the sentinel
should support pickling.

.. versionadded:: next
4 changes: 4 additions & 0 deletions Doc/data/refcounts.dat
Original file line number Diff line number Diff line change
Expand Up @@ -2037,6 +2037,10 @@ PySeqIter_Check:PyObject *:op:0:
PySeqIter_New:PyObject*::+1:
PySeqIter_New:PyObject*:seq:0:

PySentinel_New:PyObject*::+1:
PySentinel_New:const char*:name::
PySentinel_New:const char*:module_name::

PySequence_Check:int:::
PySequence_Check:PyObject*:o:0:

Expand Down
53 changes: 46 additions & 7 deletions Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ are always available. They are listed here in alphabetical order.
| | :func:`ascii` | | :func:`filter` | | :func:`map` | | **S** |
| | | | :func:`float` | | :func:`max` | | |func-set|_ |
| | **B** | | :func:`format` | | |func-memoryview|_ | | :func:`setattr` |
| | :func:`bin` | | |func-frozenset|_ | | :func:`min` | | :func:`slice` |
| | :func:`bool` | | | | | | :func:`sorted` |
| | :func:`breakpoint` | | **G** | | **N** | | :func:`staticmethod` |
| | |func-bytearray|_ | | :func:`getattr` | | :func:`next` | | |func-str|_ |
| | |func-bytes|_ | | :func:`globals` | | | | :func:`sum` |
| | | | | | **O** | | :func:`super` |
| | **C** | | **H** | | :func:`object` | | |
| | :func:`bin` | | |func-frozenset|_ | | :func:`min` | | :func:`sentinel` |
| | :func:`bool` | | | | | | :func:`slice` |
| | :func:`breakpoint` | | **G** | | **N** | | :func:`sorted` |
| | |func-bytearray|_ | | :func:`getattr` | | :func:`next` | | :func:`staticmethod` |
| | |func-bytes|_ | | :func:`globals` | | | | |func-str|_ |
| | | | | | **O** | | :func:`sum` |
| | **C** | | **H** | | :func:`object` | | :func:`super` |
| | :func:`callable` | | :func:`hasattr` | | :func:`oct` | | **T** |
| | :func:`chr` | | :func:`hash` | | :func:`open` | | |func-tuple|_ |
| | :func:`classmethod` | | :func:`help` | | :func:`ord` | | :func:`type` |
Expand Down Expand Up @@ -1827,6 +1827,45 @@ are always available. They are listed here in alphabetical order.
:func:`setattr`.


.. class:: sentinel(name, /)

Return a new unique sentinel object. *name* must be a :class:`str`, and is
used as the returned object's representation::
Comment thread
JelleZijlstra marked this conversation as resolved.

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

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

Shallow and deep copies of a sentinel object return the object itself.

Sentinels importable from their defining module by name preserve their
identity when pickled and unpickled. Sentinels that are not importable by
module and name are not picklable.

Sentinels are conventionally assigned to a variable with a matching name.
Sentinels defined in this way can be used in :term:`type hints<type hint>`::

MISSING = sentinel("MISSING")

def next_value(default: int | MISSING = MISSING):
...

Sentinel objects support the :ref:`| <bitwise>` operator for use in type expressions.

.. attribute:: __name__

The sentinel's name.

.. attribute:: __module__

The name of the module where the sentinel was created.

.. versionadded:: next


.. class:: slice(stop, /)
slice(start, stop, step=None, /)

Expand Down
14 changes: 14 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ Summary -- Release highlights
<whatsnew315-lazy-imports>`
* :pep:`814`: :ref:`Add frozendict built-in type
<whatsnew315-frozendict>`
* :pep:`661`: :ref:`Add sentinel built-in type
<whatsnew315-sentinel>`
* :pep:`799`: :ref:`A dedicated profiling package for organizing Python
profiling tools <whatsnew315-profiling-package>`
* :pep:`799`: :ref:`Tachyon: High frequency statistical sampling profiler
Expand Down Expand Up @@ -234,6 +236,18 @@ to accept also other mapping types such as :class:`~types.MappingProxyType`.
(Contributed by Victor Stinner and Donghee Na in :gh:`141510`.)


.. _whatsnew315-sentinel:

:pep:`661`: Add sentinel built-in type
--------------------------------------

A new :class:`sentinel` type is added to the :mod:`builtins` module for
creating unique sentinel values with a concise representation. Sentinel
objects preserve identity when copied, support use in type expressions with
the ``|`` operator, and can be pickled when they are importable by module and
name.


.. _whatsnew315-profiling-package:

:pep:`799`: A dedicated profiling package
Expand Down
1 change: 1 addition & 0 deletions Include/Python.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ __pragma(warning(disable: 4201))
#include "cpython/genobject.h"
#include "descrobject.h"
#include "genericaliasobject.h"
#include "sentinelobject.h"
#include "warnings.h"
#include "weakrefobject.h"
#include "structseq.h"
Expand Down
22 changes: 22 additions & 0 deletions Include/sentinelobject.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* Sentinel object interface */

#ifndef Py_SENTINELOBJECT_H
#define Py_SENTINELOBJECT_H
#ifdef __cplusplus
extern "C" {
#endif

#ifndef Py_LIMITED_API
PyAPI_DATA(PyTypeObject) PySentinel_Type;

#define PySentinel_Check(op) Py_IS_TYPE((op), &PySentinel_Type)

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

#ifdef __cplusplus
}
#endif
#endif /* !Py_SENTINELOBJECT_H */
79 changes: 79 additions & 0 deletions Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import builtins
import collections
import contextlib
import copy
import decimal
import fractions
import gc
Expand All @@ -21,6 +22,7 @@
import typing
import unittest
import warnings
import weakref
from contextlib import ExitStack
from functools import partial
from inspect import CO_COROUTINE
Expand Down Expand Up @@ -52,6 +54,10 @@

# used as proof of globals being used
A_GLOBAL_VALUE = 123
A_SENTINEL = sentinel("A_SENTINEL")

class SentinelContainer:
CLASS_SENTINEL = sentinel("SentinelContainer.CLASS_SENTINEL")

class Squares:

Expand Down Expand Up @@ -1903,6 +1909,79 @@ class C:
__repr__ = None
self.assertRaises(TypeError, repr, C())

def test_sentinel(self):
missing = sentinel("MISSING")
other = sentinel("MISSING")

self.assertIsInstance(missing, sentinel)
self.assertIs(type(missing), sentinel)
self.assertEqual(missing.__name__, "MISSING")
self.assertEqual(missing.__module__, __name__)
self.assertIsNot(missing, other)
self.assertEqual(repr(missing), "MISSING")
self.assertTrue(missing)
self.assertIs(copy.copy(missing), missing)
self.assertIs(copy.deepcopy(missing), missing)
self.assertEqual(missing, missing)
self.assertNotEqual(missing, other)
self.assertRaises(TypeError, sentinel)
self.assertRaises(TypeError, sentinel, "MISSING", "EXTRA")
self.assertRaises(TypeError, sentinel, name="MISSING")
with self.assertRaisesRegex(TypeError, "must be str"):
sentinel(1)
self.assertTrue(sentinel.__flags__ & support._TPFLAGS_IMMUTABLETYPE)
self.assertTrue(sentinel.__flags__ & support._TPFLAGS_HAVE_GC)
self.assertFalse(sentinel.__flags__ & support._TPFLAGS_BASETYPE)
with self.assertRaises(TypeError):
class SubSentinel(sentinel):
pass
with self.assertRaises(TypeError):
sentinel.attribute = "value"
with self.assertRaises(AttributeError):
missing.__name__ = "CHANGED"
with self.assertRaises(AttributeError):
missing.__module__ = "changed"

def test_sentinel_pickle(self):
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(protocol=proto):
self.assertIs(
pickle.loads(pickle.dumps(A_SENTINEL, protocol=proto)),
A_SENTINEL)
self.assertIs(
pickle.loads(pickle.dumps(
SentinelContainer.CLASS_SENTINEL, protocol=proto)),
SentinelContainer.CLASS_SENTINEL)

missing = sentinel("MISSING")
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(protocol=proto):
with self.assertRaises(pickle.PicklingError):
pickle.dumps(missing, protocol=proto)

def test_sentinel_str_subclass_name_cycle(self):
class Name(str):
pass

name = Name("MISSING")
missing = sentinel(name)
self.assertIs(missing.__name__, name)
self.assertTrue(gc.is_tracked(missing))

name.missing = missing
ref = weakref.ref(name)
del name, missing
support.gc_collect()
self.assertIsNone(ref())

def test_sentinel_union(self):
missing = sentinel("MISSING")

self.assertEqual((missing | int).__args__, (missing, int))
self.assertEqual((int | missing).__args__, (int, missing))
self.assertIs(missing | missing, missing)
self.assertEqual(repr(int | missing), "int | MISSING")

def test_round(self):
self.assertEqual(round(0.0), 0.0)
self.assertEqual(type(round(0.0)), int)
Expand Down
18 changes: 18 additions & 0 deletions Lib/test/test_capi/test_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,24 @@ def test_get_constant_borrowed(self):
self.check_get_constant(_testlimitedcapi.get_constant_borrowed)


class SentinelTest(unittest.TestCase):

def test_pysentinel_new(self):
import pickle

marker = _testcapi.pysentinel_new("CAPI_SENTINEL", __name__)
self.assertIs(type(marker), sentinel)
self.assertTrue(_testcapi.pysentinel_check(marker))
self.assertFalse(_testcapi.pysentinel_check(object()))
self.assertEqual(marker.__name__, "CAPI_SENTINEL")
self.assertEqual(marker.__module__, __name__)
self.assertEqual(repr(marker), "CAPI_SENTINEL")

globals()["CAPI_SENTINEL"] = marker
self.addCleanup(globals().pop, "CAPI_SENTINEL", None)
self.assertIs(pickle.loads(pickle.dumps(marker)), marker)


class PrintTest(unittest.TestCase):
def testPyObjectPrintObject(self):

Expand Down
26 changes: 1 addition & 25 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3122,31 +3122,7 @@ def _namedtuple_mro_entries(bases):
NamedTuple.__mro_entries__ = _namedtuple_mro_entries


class _SingletonMeta(type):
def __setattr__(cls, attr, value):
# TypeError is consistent with the behavior of NoneType
raise TypeError(
f"cannot set {attr!r} attribute of immutable type {cls.__name__!r}"
)


class _NoExtraItemsType(metaclass=_SingletonMeta):
"""The type of the NoExtraItems singleton."""

__slots__ = ()

def __new__(cls):
return globals().get("NoExtraItems") or object.__new__(cls)

def __repr__(self):
return 'typing.NoExtraItems'

def __reduce__(self):
return 'NoExtraItems'

NoExtraItems = _NoExtraItemsType()
del _NoExtraItemsType
del _SingletonMeta
NoExtraItems = sentinel("NoExtraItems")


def _get_typeddict_qualifiers(annotation_type):
Expand Down
2 changes: 2 additions & 0 deletions Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ OBJECT_OBJS= \
Objects/picklebufobject.o \
Objects/rangeobject.o \
Objects/setobject.o \
Objects/sentinelobject.o \
Objects/sliceobject.o \
Objects/structseq.o \
Objects/templateobject.o \
Expand Down Expand Up @@ -1238,6 +1239,7 @@ PYTHON_HEADERS= \
$(srcdir)/Include/rangeobject.h \
$(srcdir)/Include/refcount.h \
$(srcdir)/Include/setobject.h \
$(srcdir)/Include/sentinelobject.h \
$(srcdir)/Include/sliceobject.h \
$(srcdir)/Include/structmember.h \
$(srcdir)/Include/structseq.h \
Expand Down
19 changes: 19 additions & 0 deletions Modules/_testcapi/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,23 @@ pyobject_dump(PyObject *self, PyObject *args)
Py_RETURN_NONE;
}

static PyObject *
pysentinel_new(PyObject *self, PyObject *args)
{
const char *name;
const char *module_name;
if (!PyArg_ParseTuple(args, "ss", &name, &module_name)) {
return NULL;
}
return PySentinel_New(name, module_name);
}

static PyObject *
pysentinel_check(PyObject *self, PyObject *obj)
{
return PyBool_FromLong(PySentinel_Check(obj));
}


static PyMethodDef test_methods[] = {
{"call_pyobject_print", call_pyobject_print, METH_VARARGS},
Expand Down Expand Up @@ -585,6 +602,8 @@ static PyMethodDef test_methods[] = {
{"clear_managed_dict", clear_managed_dict, METH_O, NULL},
{"is_uniquely_referenced", is_uniquely_referenced, METH_O},
{"pyobject_dump", pyobject_dump, METH_VARARGS},
{"pysentinel_new", pysentinel_new, METH_VARARGS},
{"pysentinel_check", pysentinel_check, METH_O},
{NULL},
};

Expand Down
Loading
Loading