-
-
Notifications
You must be signed in to change notification settings - Fork 34.5k
gh-148829: Implement PEP 661 #148831
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
gh-148829: Implement PEP 661 #148831
Changes from 18 commits
559e527
ededfb7
39f364c
0adf314
e4a106a
e5b8789
bdcb400
cc4545c
be58215
91de05b
4a970a6
9cfd101
a851235
2beb1c2
e2d92a1
53a84b6
396356e
bfa98a1
465061a
70c04a2
7c276c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -112,6 +112,7 @@ Other Objects | |
| picklebuffer.rst | ||
| weakref.rst | ||
| capsule.rst | ||
| sentinel.rst | ||
| frame.rst | ||
| gen.rst | ||
| coro.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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,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. | ||
|
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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, /) | ||
|
|
||
|
|
||
| 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 | ||
|
picnixz marked this conversation as resolved.
|
||
|
|
||
| #ifdef __cplusplus | ||
| } | ||
| #endif | ||
| #endif /* !Py_SENTINELOBJECT_H */ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| import builtins | ||
| import collections | ||
| import contextlib | ||
| import copy | ||
| import decimal | ||
| import fractions | ||
| import gc | ||
|
|
@@ -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 | ||
|
|
@@ -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: | ||
|
|
||
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
|
|
||
|
|
||
| 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. |
Uh oh!
There was an error while loading. Please reload this page.