]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-148829: Implement PEP 661 (#148831)
authorJelle Zijlstra <jelle.zijlstra@gmail.com>
Tue, 28 Apr 2026 02:28:30 +0000 (19:28 -0700)
committerGitHub <noreply@github.com>
Tue, 28 Apr 2026 02:28:30 +0000 (19:28 -0700)
Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>
Co-authored-by: Pieter Eendebak <pieter.eendebak@gmail.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
24 files changed:
Doc/c-api/concrete.rst
Doc/c-api/sentinel.rst [new file with mode: 0644]
Doc/data/refcounts.dat
Doc/library/functions.rst
Doc/whatsnew/3.15.rst
Include/Python.h
Include/sentinelobject.h [new file with mode: 0644]
Lib/test/pickletester.py
Lib/test/test_builtin.py
Lib/test/test_capi/test_object.py
Lib/typing.py
Makefile.pre.in
Misc/NEWS.d/next/Core_and_Builtins/2026-04-21-06-43-32.gh-issue-148829.GtIrYO.rst [new file with mode: 0644]
Modules/_testcapi/object.c
Objects/clinic/sentinelobject.c.h [new file with mode: 0644]
Objects/object.c
Objects/sentinelobject.c [new file with mode: 0644]
Objects/unionobject.c
PCbuild/_freeze_module.vcxproj
PCbuild/_freeze_module.vcxproj.filters
PCbuild/pythoncore.vcxproj
PCbuild/pythoncore.vcxproj.filters
Python/bltinmodule.c
Tools/c-analyzer/cpython/globals-to-fix.tsv

index 1746fe95eaaca933c46be541bff674f0597c91dc..3f38411a52de6b081f3247ff054359269765d778 100644 (file)
@@ -112,6 +112,7 @@ Other Objects
    picklebuffer.rst
    weakref.rst
    capsule.rst
+   sentinel.rst
    frame.rst
    gen.rst
    coro.rst
diff --git a/Doc/c-api/sentinel.rst b/Doc/c-api/sentinel.rst
new file mode 100644 (file)
index 0000000..710ded5
--- /dev/null
@@ -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
index 2a6e6b963134bb77e20731a7293981a7fd385f5e..663b79e45eec17dfee43e19be0a6f10bc6a8d39a 100644 (file)
@@ -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:
 
index 119141d2e6daf391bb27032d8139d29601b3ab78..aa99d198e436d575a05bd4517a38c3242fa38a62 100644 (file)
@@ -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`         |
@@ -1827,6 +1827,61 @@ 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 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. Other sentinels, such as those
+   defined in a function scope, are not picklable. 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
+
+   Sentinel objects have the following attributes:
+
+   .. 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, /)
 
index 405d388af487e8156e41acbdc09088f438f118d5..0a96a970ba2329e882d232cbe58d539ebff239da 100644 (file)
@@ -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
@@ -247,6 +249,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
index 8b76195b32099836f7d212b157158be9bdcf4a8e..1272e2464f91d1e2347e77c7a40f7eeaf62df5cc 100644 (file)
@@ -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"
diff --git a/Include/sentinelobject.h b/Include/sentinelobject.h
new file mode 100644 (file)
index 0000000..9d85777
--- /dev/null
@@ -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 */
index 6366f12257f3b5ae5f51b2999f9e3d4ef160e0f0..c2018c9785b9b305e2ee3142926b112e7144d781 100644 (file)
@@ -3244,6 +3244,7 @@ class AbstractPickleTests:
             '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):
index 844656eb0e2c2e436c52b58e553fff177efdc837..e323742665234ce53d94fb2baa53909052bb7966 100644 (file)
@@ -4,6 +4,7 @@ import ast
 import builtins
 import collections
 import contextlib
+import copy
 import decimal
 import fractions
 import gc
@@ -21,6 +22,7 @@ import types
 import typing
 import unittest
 import warnings
+import weakref
 from contextlib import ExitStack
 from functools import partial
 from inspect import CO_COROUTINE
@@ -52,6 +54,10 @@ HAVE_DOUBLE_ROUNDING = (x + y == 1e16 + 4)
 
 # 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:
 
@@ -1903,6 +1909,98 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
             __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"
+        with self.assertRaises(AttributeError):
+            del missing.__name__
+        with self.assertRaises(AttributeError):
+            del missing.__module__
+
+    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.assertIsInstance(missing | int, typing.Union)
+        self.assertEqual((missing | int).__args__, (missing, int))
+        self.assertIsInstance(int | missing, typing.Union)
+        self.assertEqual((int | missing).__args__, (int, missing))
+        self.assertIs(missing | missing, missing)
+        self.assertEqual(repr(int | missing), "int | MISSING")
+        self.assertIsInstance(missing | None, typing.Union)
+        self.assertEqual((missing | None).__args__, (missing, type(None)))
+        self.assertIsInstance(None | missing, typing.Union)
+        self.assertEqual((None | missing).__args__, (type(None), missing))
+        self.assertIsInstance(missing | list[int], typing.Union)
+        self.assertEqual((missing | list[int]).__args__, (missing, list[int]))
+        self.assertIsInstance(missing | (int | str), typing.Union)
+        self.assertEqual((missing | (int | str)).__args__, (missing, int, str))
+
+        with self.assertRaises(TypeError):
+            missing | 1
+        with self.assertRaises(TypeError):
+            1 | missing
+
     def test_round(self):
         self.assertEqual(round(0.0), 0.0)
         self.assertEqual(type(round(0.0)), int)
index 67572ab1ba268d0223ed572209621a5a082637ae..635deaa73f7efab2ab8ba69b4b5abe1726f13eb3 100644 (file)
@@ -1,5 +1,6 @@
 import enum
 import os
+import pickle
 import sys
 import textwrap
 import unittest
@@ -63,6 +64,27 @@ class GetConstantTest(unittest.TestCase):
         self.check_get_constant(_testlimitedcapi.get_constant_borrowed)
 
 
+class SentinelTest(unittest.TestCase):
+
+    def test_pysentinel_new(self):
+        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):
 
index 46e7122b6c91c546ee0ad2b549d29228430b9096..e7563a53878da50c46a8f4dc3bf9b465aefecb62 100644 (file)
@@ -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")
 
 
 def _get_typeddict_qualifiers(annotation_type):
index 8b46db33a2ac1884332d97263edd4c94c9b2391a..2ce53c6a8162128a2200767b40dc22b2b59ea2a4 100644 (file)
@@ -560,6 +560,7 @@ OBJECT_OBJS=        \
                Objects/obmalloc.o \
                Objects/picklebufobject.o \
                Objects/rangeobject.o \
+               Objects/sentinelobject.o \
                Objects/setobject.o \
                Objects/sliceobject.o \
                Objects/structseq.o \
@@ -1240,6 +1241,7 @@ PYTHON_HEADERS= \
                $(srcdir)/Include/pytypedefs.h \
                $(srcdir)/Include/rangeobject.h \
                $(srcdir)/Include/refcount.h \
+               $(srcdir)/Include/sentinelobject.h \
                $(srcdir)/Include/setobject.h \
                $(srcdir)/Include/sliceobject.h \
                $(srcdir)/Include/structmember.h \
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-21-06-43-32.gh-issue-148829.GtIrYO.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-21-06-43-32.gh-issue-148829.GtIrYO.rst
new file mode 100644 (file)
index 0000000..3d9b4fa
--- /dev/null
@@ -0,0 +1,2 @@
+Add :class:`sentinel`, implementing :pep:`661`. PEP by Tal Einat; patch by
+Jelle Zijlstra.
index 9160005e00654f997f0386d822ad3fb3a4b26ca1..6e5c8dcbb725fa5fb4f8bba86e634a1b24d1a563 100644 (file)
@@ -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 = NULL;
+    if (!PyArg_ParseTuple(args, "s|s", &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},
@@ -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},
 };
 
diff --git a/Objects/clinic/sentinelobject.c.h b/Objects/clinic/sentinelobject.c.h
new file mode 100644 (file)
index 0000000..51fd35a
--- /dev/null
@@ -0,0 +1,34 @@
+/*[clinic input]
+preserve
+[clinic start generated code]*/
+
+#include "pycore_modsupport.h"    // _PyArg_CheckPositional()
+
+static PyObject *
+sentinel_new_impl(PyTypeObject *type, PyObject *name);
+
+static PyObject *
+sentinel_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
+{
+    PyObject *return_value = NULL;
+    PyTypeObject *base_tp = &PySentinel_Type;
+    PyObject *name;
+
+    if ((type == base_tp || type->tp_init == base_tp->tp_init) &&
+        !_PyArg_NoKeywords("sentinel", kwargs)) {
+        goto exit;
+    }
+    if (!_PyArg_CheckPositional("sentinel", PyTuple_GET_SIZE(args), 1, 1)) {
+        goto exit;
+    }
+    if (!PyUnicode_Check(PyTuple_GET_ITEM(args, 0))) {
+        _PyArg_BadArgument("sentinel", "argument 1", "str", PyTuple_GET_ITEM(args, 0));
+        goto exit;
+    }
+    name = PyTuple_GET_ITEM(args, 0);
+    return_value = sentinel_new_impl(type, name);
+
+exit:
+    return return_value;
+}
+/*[clinic end generated code: output=7f28fc0bf0259cba input=a9049054013a1b77]*/
index 4fa20470601eb3db1bc44772898bf68c340e5aff..e6a764435bc29206eab03fa454cdad42e159098f 100644 (file)
@@ -2597,6 +2597,7 @@ static PyTypeObject* static_types[] = {
     &PyRange_Type,
     &PyReversed_Type,
     &PySTEntry_Type,
+    &PySentinel_Type,
     &PySeqIter_Type,
     &PySetIter_Type,
     &PySet_Type,
diff --git a/Objects/sentinelobject.c b/Objects/sentinelobject.c
new file mode 100644 (file)
index 0000000..e7e9f60
--- /dev/null
@@ -0,0 +1,196 @@
+/* Sentinel object implementation */
+
+#include "Python.h"
+#include "descrobject.h"          // PyMemberDef
+#include "pycore_ceval.h"         // _PyThreadState_GET()
+#include "pycore_interpframe.h"   // _PyFrame_IsIncomplete()
+#include "pycore_object.h"        // _PyObject_GC_TRACK/UNTRACK()
+#include "pycore_stackref.h"      // PyStackRef_AsPyObjectBorrow()
+#include "pycore_tuple.h"         // _PyTuple_FromPair
+#include "pycore_typeobject.h"    // _Py_BaseObject_RichCompare()
+#include "pycore_unionobject.h"   // _Py_union_type_or()
+
+typedef struct {
+    PyObject_HEAD
+    PyObject *name;
+    PyObject *module;
+} sentinelobject;
+
+#define sentinelobject_CAST(op) \
+    (assert(PySentinel_Check(op)), _Py_CAST(sentinelobject *, (op)))
+
+/*[clinic input]
+class sentinel "sentinelobject *" "&PySentinel_Type"
+[clinic start generated code]*/
+/*[clinic end generated code: output=da39a3ee5e6b4b0d input=8b88f8268d3b5775]*/
+
+#include "clinic/sentinelobject.c.h"
+
+
+static PyObject *
+caller(void)
+{
+    _PyInterpreterFrame *f = _PyThreadState_GET()->current_frame;
+    if (f == NULL || PyStackRef_IsNull(f->f_funcobj)) {
+        assert(!PyErr_Occurred());
+        Py_RETURN_NONE;
+    }
+    PyFunctionObject *func = _PyFrame_GetFunction(f);
+    assert(PyFunction_Check(func));
+    PyObject *r = PyFunction_GetModule((PyObject *)func);
+    if (!r) {
+        assert(!PyErr_Occurred());
+        Py_RETURN_NONE;
+    }
+    return Py_NewRef(r);
+}
+
+static PyObject *
+sentinel_new_with_module(PyTypeObject *type, PyObject *name, PyObject *module)
+{
+    assert(PyUnicode_Check(name));
+
+    sentinelobject *self = PyObject_GC_New(sentinelobject, type);
+    if (self == NULL) {
+        return NULL;
+    }
+    self->name = Py_NewRef(name);
+    self->module = Py_NewRef(module);
+    _PyObject_GC_TRACK(self);
+    return (PyObject *)self;
+}
+
+/*[clinic input]
+@classmethod
+sentinel.__new__ as sentinel_new
+
+    name: object(subclass_of='&PyUnicode_Type')
+    /
+[clinic start generated code]*/
+
+static PyObject *
+sentinel_new_impl(PyTypeObject *type, PyObject *name)
+/*[clinic end generated code: output=4af55c6048bed30d input=3ab75704f39c119c]*/
+{
+    PyObject *module = caller();
+    PyObject *self = sentinel_new_with_module(type, name, module);
+    Py_DECREF(module);
+    return self;
+}
+
+PyObject *
+PySentinel_New(const char *name, const char *module_name)
+{
+    PyObject *name_obj = PyUnicode_FromString(name);
+    if (name_obj == NULL) {
+        return NULL;
+    }
+    PyObject *module_obj = module_name == NULL
+        ? Py_None
+        : PyUnicode_FromString(module_name);
+    if (module_obj == NULL) {
+        Py_DECREF(name_obj);
+        return NULL;
+    }
+
+    PyObject *sentinel = sentinel_new_with_module(
+        &PySentinel_Type, name_obj, module_obj);
+    Py_DECREF(module_obj);
+    Py_DECREF(name_obj);
+    return sentinel;
+}
+
+static int
+sentinel_clear(PyObject *op)
+{
+    sentinelobject *self = sentinelobject_CAST(op);
+    Py_CLEAR(self->name);
+    Py_CLEAR(self->module);
+    return 0;
+}
+
+static void
+sentinel_dealloc(PyObject *op)
+{
+    _PyObject_GC_UNTRACK(op);
+    (void)sentinel_clear(op);
+    Py_TYPE(op)->tp_free(op);
+}
+
+static int
+sentinel_traverse(PyObject *op, visitproc visit, void *arg)
+{
+    sentinelobject *self = sentinelobject_CAST(op);
+    Py_VISIT(self->name);
+    Py_VISIT(self->module);
+    return 0;
+}
+
+static PyObject *
+sentinel_repr(PyObject *op)
+{
+    sentinelobject *self = sentinelobject_CAST(op);
+    return Py_NewRef(self->name);
+}
+
+static PyObject *
+sentinel_copy(PyObject *self, PyObject *Py_UNUSED(ignored))
+{
+    return Py_NewRef(self);
+}
+
+static PyObject *
+sentinel_deepcopy(PyObject *self, PyObject *Py_UNUSED(memo))
+{
+    return Py_NewRef(self);
+}
+
+static PyObject *
+sentinel_reduce(PyObject *op, PyObject *Py_UNUSED(ignored))
+{
+    sentinelobject *self = sentinelobject_CAST(op);
+    return Py_NewRef(self->name);
+}
+
+static PyMethodDef sentinel_methods[] = {
+    {"__copy__", sentinel_copy, METH_NOARGS, NULL},
+    {"__deepcopy__", sentinel_deepcopy, METH_O, NULL},
+    {"__reduce__", sentinel_reduce, METH_NOARGS, NULL},
+    {NULL, NULL}
+};
+
+static PyMemberDef sentinel_members[] = {
+    {"__name__", Py_T_OBJECT_EX, offsetof(sentinelobject, name), Py_READONLY},
+    {"__module__", Py_T_OBJECT_EX, offsetof(sentinelobject, module), Py_READONLY},
+    {NULL}
+};
+
+static PyNumberMethods sentinel_as_number = {
+    .nb_or = _Py_union_type_or,
+};
+
+PyDoc_STRVAR(sentinel_doc,
+"sentinel(name, /)\n"
+"--\n\n"
+"Create a unique sentinel object with the given name.");
+
+PyTypeObject PySentinel_Type = {
+    PyVarObject_HEAD_INIT(&PyType_Type, 0)
+    .tp_name = "sentinel",
+    .tp_basicsize = sizeof(sentinelobject),
+    .tp_dealloc = sentinel_dealloc,
+    .tp_repr = sentinel_repr,
+    .tp_as_number = &sentinel_as_number,
+    .tp_hash = PyObject_GenericHash,
+    .tp_getattro = PyObject_GenericGetAttr,
+    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE
+                | Py_TPFLAGS_HAVE_GC,
+    .tp_doc = sentinel_doc,
+    .tp_traverse = sentinel_traverse,
+    .tp_clear = sentinel_clear,
+    .tp_richcompare = _Py_BaseObject_RichCompare,
+    .tp_methods = sentinel_methods,
+    .tp_members = sentinel_members,
+    .tp_new = sentinel_new,
+    .tp_free = PyObject_GC_Del,
+};
index d33d581f049c5bf87abdc853fe55f4dc1aa78d6f..0f6b1e44bc2402c0a60b7ef37a288d20af0c8084 100644 (file)
@@ -245,6 +245,7 @@ is_unionable(PyObject *obj)
 {
     if (obj == Py_None ||
         PyType_Check(obj) ||
+        PySentinel_Check(obj) ||
         _PyGenericAlias_Check(obj) ||
         _PyUnion_Check(obj) ||
         Py_IS_TYPE(obj, &_PyTypeAlias_Type)) {
index 38236922a523db52bdd9e42c8b2801e09f9030a9..953973a2ad32df7bce6ede5cf3ae9e75a7e68bbb 100644 (file)
     <ClCompile Include="..\Objects\odictobject.c" />
     <ClCompile Include="..\Objects\picklebufobject.c" />
     <ClCompile Include="..\Objects\rangeobject.c" />
+    <ClCompile Include="..\Objects\sentinelobject.c" />
     <ClCompile Include="..\Objects\setobject.c" />
     <ClCompile Include="..\Objects\sliceobject.c" />
     <ClCompile Include="..\Objects\structseq.c" />
index 73861dbb0c9e7e3e1b62ea72586757f659887ba4..13db4d93f54518db387af372b5962f571b99788a 100644 (file)
     <ClCompile Include="..\Objects\rangeobject.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\Objects\sentinelobject.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="..\Objects\setobject.c">
       <Filter>Source Files</Filter>
     </ClCompile>
index 07305add81d055a0803a98320a09f03b77214403..fb9217fee8bd737e5d3f5afc65eca291edeffe95 100644 (file)
     <ClInclude Include="..\Include\pytypedefs.h" />
     <ClInclude Include="..\Include\rangeobject.h" />
     <ClInclude Include="..\Include\refcount.h" />
+    <ClInclude Include="..\Include\sentinelobject.h" />
     <ClInclude Include="..\Include\setobject.h" />
     <ClInclude Include="..\Include\sliceobject.h" />
     <ClInclude Include="..\Include\structmember.h" />
     <ClCompile Include="..\Objects\odictobject.c" />
     <ClCompile Include="..\Objects\picklebufobject.c" />
     <ClCompile Include="..\Objects\rangeobject.c" />
+    <ClCompile Include="..\Objects\sentinelobject.c" />
     <ClCompile Include="..\Objects\setobject.c" />
     <ClCompile Include="..\Objects\sliceobject.c" />
     <ClCompile Include="..\Objects\structseq.c" />
index 629f063861de9a1614e95dba060d97dabb702d6b..1e1d085cd75511efe34ad8bdaf5787905a698f50 100644 (file)
     <ClInclude Include="..\Include\runtime_structs.h">
       <Filter>Include</Filter>
     </ClInclude>
+    <ClInclude Include="..\Include\sentinelobject.h">
+      <Filter>Include</Filter>
+    </ClInclude>
     <ClInclude Include="..\Include\setobject.h">
       <Filter>Include</Filter>
     </ClInclude>
     <ClCompile Include="..\Objects\rangeobject.c">
       <Filter>Objects</Filter>
     </ClCompile>
+    <ClCompile Include="..\Objects\sentinelobject.c">
+      <Filter>Objects</Filter>
+    </ClCompile>
     <ClCompile Include="..\Objects\setobject.c">
       <Filter>Objects</Filter>
     </ClCompile>
index 16413d784cc87cdeaf93a21983544155baaff944..35b30a243318cc3da3941a72456e9e399bcfd253 100644 (file)
@@ -3555,6 +3555,7 @@ _PyBuiltin_Init(PyInterpreterState *interp)
     SETBUILTIN("object",                &PyBaseObject_Type);
     SETBUILTIN("range",                 &PyRange_Type);
     SETBUILTIN("reversed",              &PyReversed_Type);
+    SETBUILTIN("sentinel",              &PySentinel_Type);
     SETBUILTIN("set",                   &PySet_Type);
     SETBUILTIN("slice",                 &PySlice_Type);
     SETBUILTIN("staticmethod",          &PyStaticMethod_Type);
index 74ca562824012b9c751b706aa557a23cdcdd0480..db575d870be5c53705d6e860004c25a0bd820ed2 100644 (file)
@@ -83,6 +83,7 @@ Objects/picklebufobject.c     -       PyPickleBuffer_Type     -
 Objects/rangeobject.c  -       PyLongRangeIter_Type    -
 Objects/rangeobject.c  -       PyRangeIter_Type        -
 Objects/rangeobject.c  -       PyRange_Type    -
+Objects/sentinelobject.c       -       PySentinel_Type -
 Objects/setobject.c    -       PyFrozenSet_Type        -
 Objects/setobject.c    -       PySetIter_Type  -
 Objects/setobject.c    -       PySet_Type      -