Skip to content
Open
Show file tree
Hide file tree
Changes from 18 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
35 changes: 35 additions & 0 deletions Doc/c-api/sentinel.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.. 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*.
*name* must not be ``NULL``. If *module_name* is ``NULL``, :attr:`~sentinel.__module__`
is set to ``None``.
Return ``NULL`` with an exception set on failure.

For pickling to work, *module_name* must be the name of an importable
module, and the sentinel must be accessible from that module under a
path matching *name*. Pickle treats *name* as a global variable name
in *module_name* (see :meth:`object.__reduce__`).

.. 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
70 changes: 63 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,62 @@ 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::

>>> 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.
Comment thread
JelleZijlstra marked this conversation as resolved.
Outdated
Comment thread
sunmy2019 marked this conversation as resolved.
Outdated

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.

:mod:`Pickling <pickle>` is supported for sentinel objects that are
placed in the global scope of a module under a name matching the sentinel's
name, and for sentinels placed in class scopes with a name matching the
:term:`qualified name` of the sentinel. The identity of the sentinel is preserved
after pickling::

import pickle

PICKLABLE = sentinel("PICKLABLE")

assert pickle.loads(pickle.dumps(PICKLABLE)) is PICKLABLE

class Cls:
PICKLABLE = sentinel("Cls.PICKLABLE")

assert pickle.loads(pickle.dumps(Cls.PICKLABLE)) is Cls.PICKLABLE

.. attribute:: __name__

The sentinel's name.

.. attribute:: __module__

The name of the module where the sentinel was created.
Comment on lines +1871 to +1880
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe indicate something like "A sentinel object exposes the following read-only attributes" so that the paragraph about pickle is not glued to it.


.. versionadded:: next


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

Expand Down
16 changes: 16 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 @@ -235,6 +237,20 @@ 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.

(PEP by Tal Einat; contributed by Jelle Zijlstra in :gh:`148829`.)


.. _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
Comment thread
picnixz marked this conversation as resolved.

#ifdef __cplusplus
}
#endif
#endif /* !Py_SENTINELOBJECT_H */
1 change: 1 addition & 0 deletions Lib/test/pickletester.py
Original file line number Diff line number Diff line change
Expand Up @@ -3244,6 +3244,7 @@ def test_builtin_types(self):
'BuiltinImporter': (3, 3),
'str': (3, 4), # not interoperable with Python < 3.4
'frozendict': (3, 15),
'sentinel': (3, 15),
}
for t in builtins.__dict__.values():
if isinstance(t, type) and not issubclass(t, BaseException):
Expand Down
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"
Comment on lines +1942 to +1943
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add tests to check that we can't delete those attributes either


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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a test where the RHS/LHS is not a type?

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
23 changes: 23 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,29 @@ 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can put this at the top-level. It's not really important that imports are fast in test modules.


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")

no_module = _testcapi.pysentinel_new("NO_MODULE")
self.assertIs(type(no_module), sentinel)
self.assertEqual(no_module.__name__, "NO_MODULE")
self.assertIs(no_module.__module__, None)

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 @@ -3150,31 +3150,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")
Comment thread
JelleZijlstra marked this conversation as resolved.


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 @@ -560,6 +560,7 @@ OBJECT_OBJS= \
Objects/obmalloc.o \
Objects/picklebufobject.o \
Objects/rangeobject.o \
Objects/sentinelobject.o \
Comment thread
JelleZijlstra marked this conversation as resolved.
Objects/setobject.o \
Objects/sliceobject.o \
Objects/structseq.o \
Expand Down Expand Up @@ -1240,6 +1241,7 @@ PYTHON_HEADERS= \
$(srcdir)/Include/pytypedefs.h \
$(srcdir)/Include/rangeobject.h \
$(srcdir)/Include/refcount.h \
$(srcdir)/Include/sentinelobject.h \
Comment thread
JelleZijlstra marked this conversation as resolved.
$(srcdir)/Include/setobject.h \
$(srcdir)/Include/sliceobject.h \
$(srcdir)/Include/structmember.h \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :class:`sentinel`, implementing :pep:`661`. PEP by Tal Einat; patch by
Jelle Zijlstra.
Loading
Loading