From b660bf8a029ba3c5121aaf7eba2bf9151d49e2d1 Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:24:20 +0300 Subject: [PATCH 01/10] Use "sentinellib" for the new module name This avoids breaking code using the existing "sentinels" PyPI package. --- peps/pep-0661.rst | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/peps/pep-0661.rst b/peps/pep-0661.rst index 021b96430df..ac2c50ec7d3 100644 --- a/peps/pep-0661.rst +++ b/peps/pep-0661.rst @@ -142,12 +142,12 @@ all of these criteria (see `Reference Implementation`_). Specification ============= -A new ``Sentinel`` class will be added to a new ``sentinels`` module. +A new ``Sentinel`` class will be added to a new ``sentinellib`` module. Its initializer will accept a single required argument, the name of the sentinel object, and three optional arguments: the repr of the object, its boolean value, and the name of its module:: - >>> from sentinels import Sentinel + >>> from sentinellib import Sentinel >>> NotGiven = Sentinel('NotGiven') >>> NotGiven @@ -240,17 +240,7 @@ methods, returning :py:class:`typing.Union` objects. Backwards Compatibility ======================= -While not breaking existing code, adding a new "sentinels" stdlib module could -cause some confusion with regard to existing modules named "sentinels", and -specifically with the "sentinels" package on PyPI. - -The existing "sentinels" package on PyPI [10]_ appears to be abandoned, with -the latest release being made on Aug. 2016. Therefore, using this name for a -new stdlib module seems reasonable. - -If and when this PEP is accepted, it may be worth verifying if this has indeed -been abandoned, and if so asking to transfer ownership to the CPython -maintainers to reduce the potential for confusion with the new stdlib module. +This proposal should have no backwards compatibility implications. How to Teach This @@ -482,7 +472,6 @@ Footnotes .. [7] `Reference implementation at the taleinat/python-stdlib-sentinels GitHub repo `_ .. [8] `bpo-35712: Make NotImplemented unusable in boolean context `_ .. [9] `Discussion thread about type signatures for these sentinels on the typing-sig mailing list `_ -.. [10] `sentinels package on PyPI `_ Copyright From f96e67e062ac11414c6aa178e55bc398ad3abe22 Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:31:36 +0300 Subject: [PATCH 02/10] Change type annotations to use the bare name rather than Literal --- peps/pep-0661.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/peps/pep-0661.rst b/peps/pep-0661.rst index ac2c50ec7d3..cc96eab9158 100644 --- a/peps/pep-0661.rst +++ b/peps/pep-0661.rst @@ -411,6 +411,17 @@ idiom were unpopular, with the highest-voted option being voted for by only 25% of the voters. +Using ``typing.Literal`` in type annotations +-------------------------------------------- + +This was suggested by several people in discussions and is what this PEP +first went with. However, it was pointed out that this would cause potential +confusion, due to e.g. ``Literal["MISSING"]`` referring to the string value +``"MISSING"`` rather than being a forward-reference to a sentinel value +``MISSING``. Using the bare name was also suggested often in discussions, and +follows the precedent and well-known pattern set by ``None``. + + Additional Notes ================ From bdcc767919bdad6f3f69c68907d19e8bfee2db9a Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Sun, 26 Jan 2025 23:00:34 +0200 Subject: [PATCH 03/10] Use sys._getframemodulename() instead of sys._getframe() in inline reference impl --- peps/pep-0661.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/peps/pep-0661.rst b/peps/pep-0661.rst index cc96eab9158..0edb3b5e721 100644 --- a/peps/pep-0661.rst +++ b/peps/pep-0661.rst @@ -272,10 +272,8 @@ simplified version follows:: repr = str(repr) if repr else f'<{name.split(".")[-1]}>' bool_value = bool(bool_value) if module_name is None: - try: - module_name = \ - sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): + module_name = sys._getframemodulename(1) + if module_name is None: module_name = __name__ registry_key = f'{module_name}-{name}' From a6255fb93472da05b73f8e067cb9caa552596375 Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Sun, 26 Jan 2025 23:06:46 +0200 Subject: [PATCH 04/10] Simplify, dropping support for custom repr and setting truthiness --- peps/pep-0661.rst | 47 ++++++++++++++++++++--------------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/peps/pep-0661.rst b/peps/pep-0661.rst index 0edb3b5e721..4c5e740c80c 100644 --- a/peps/pep-0661.rst +++ b/peps/pep-0661.rst @@ -143,22 +143,11 @@ Specification ============= A new ``Sentinel`` class will be added to a new ``sentinellib`` module. -Its initializer will accept a single required argument, the name of the -sentinel object, and three optional arguments: the repr of the object, its -boolean value, and the name of its module:: >>> from sentinellib import Sentinel - >>> NotGiven = Sentinel('NotGiven') - >>> NotGiven - - >>> MISSING = Sentinel('MISSING', repr='mymodule.MISSING') + >>> MISSING = Sentinel('MISSING') >>> MISSING - mymodule.MISSING - >>> MEGA = Sentinel('MEGA', - repr='', - bool_value=False, - module_name='mymodule') - + MISSING Checking if a value is such a sentinel *should* be done using the ``is`` operator, as is recommended for ``None``. Equality checks using ``==`` will @@ -177,12 +166,13 @@ with the same name in different modules will be distinct from each other. Creating a copy of a sentinel object, such as by using ``copy.copy()`` or by pickling and unpickling, will return the same object. -The ``module_name`` optional argument should normally not need to be supplied, -as ``Sentinel()`` will usually be able to recognize the module in which it was -called. ``module_name`` should be supplied only in unusual cases when this -automatic recognition does not work as intended, such as perhaps when using -Jython or IronPython. This parallels the designs of ``Enum`` and -``namedtuple``. For more details, see :pep:`435`. +``Sentinel()`` will also accept a single optional argument, ``module_name``. +This should normally not need to be supplied, as ``Sentinel()`` will usually +be able to recognize the module in which it was called. ``module_name`` +should be supplied only in unusual cases when this automatic recognition does +not work as intended, such as perhaps when using Jython or IronPython. This +parallels the designs of ``Enum`` and ``namedtuple``. For more details, see +:pep:`435`. The ``Sentinel`` class may not be sub-classed, to avoid overly-clever uses based on it, such as attempts to use it as a base for implementing singletons. @@ -267,10 +257,9 @@ simplified version follows:: class Sentinel: """Unique sentinel values.""" - def __new__(cls, name, repr=None, bool_value=True, module_name=None): + def __new__(cls, name, module_name=None): name = str(name) - repr = str(repr) if repr else f'<{name.split(".")[-1]}>' - bool_value = bool(bool_value) + if module_name is None: module_name = sys._getframemodulename(1) if module_name is None: @@ -284,18 +273,14 @@ simplified version follows:: sentinel = super().__new__(cls) sentinel._name = name - sentinel._repr = repr - sentinel._bool_value = bool_value sentinel._module_name = module_name + sentinel._repr = name.split(".")[-1] return _registry.setdefault(registry_key, sentinel) def __repr__(self): return self._repr - def __bool__(self): - return self._bool_value - def __reduce__(self): return ( self.__class__, @@ -409,6 +394,14 @@ idiom were unpopular, with the highest-voted option being voted for by only 25% of the voters. +Allowing customization of repr +------------------------------ + +This was desirable to allow using this for existing sentinel values without +changing their repr. However, this was eventually dropped as it wasn't +considered worth the added complexity. + + Using ``typing.Literal`` in type annotations -------------------------------------------- From a54b989bac429111e3aa865e2e93c96c784de099 Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Sun, 26 Jan 2025 23:08:30 +0200 Subject: [PATCH 05/10] Tweak some wording --- peps/pep-0661.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/peps/pep-0661.rst b/peps/pep-0661.rst index 4c5e740c80c..1aa5d746019 100644 --- a/peps/pep-0661.rst +++ b/peps/pep-0661.rst @@ -155,13 +155,14 @@ also work as expected, returning ``True`` only when the object is compared with itself. Identity checks such as ``if value is MISSING:`` should usually be used rather than boolean checks such as ``if value:`` or ``if not value:``. -Sentinel instances are truthy by default, unlike ``None``. This parallels the -default for arbitrary classes, as well as the boolean value of ``Ellipsis``. +Sentinel instances are "truthy", i.e. boolean evaluation will result in +``True``. This parallels the default for arbitrary classes, as well as the +boolean value of ``Ellipsis``. This is unlike ``None``, which is "falsy". The names of sentinels are unique within each module. When calling ``Sentinel()`` in a module where a sentinel with that name was already defined, the existing sentinel with that name will be returned. Sentinels -with the same name in different modules will be distinct from each other. +with the same name from different modules will be distinct from each other. Creating a copy of a sentinel object, such as by using ``copy.copy()`` or by pickling and unpickling, will return the same object. @@ -174,10 +175,8 @@ not work as intended, such as perhaps when using Jython or IronPython. This parallels the designs of ``Enum`` and ``namedtuple``. For more details, see :pep:`435`. -The ``Sentinel`` class may not be sub-classed, to avoid overly-clever uses -based on it, such as attempts to use it as a base for implementing singletons. -It is considered important that the addition of Sentinel to the stdlib should -add minimal complexity. +The ``Sentinel`` class may not be sub-classed, to avoid the greater complexity +of supporting subclassing. Ordering comparisons are undefined for sentinel objects. From 5be312a679e1546cbc8913b146c7544ae0797247 Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Sun, 26 Jan 2025 23:34:01 +0200 Subject: [PATCH 06/10] Another minor wording tweak --- peps/pep-0661.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/peps/pep-0661.rst b/peps/pep-0661.rst index 1aa5d746019..1481c55930e 100644 --- a/peps/pep-0661.rst +++ b/peps/pep-0661.rst @@ -162,7 +162,8 @@ boolean value of ``Ellipsis``. This is unlike ``None``, which is "falsy". The names of sentinels are unique within each module. When calling ``Sentinel()`` in a module where a sentinel with that name was already defined, the existing sentinel with that name will be returned. Sentinels -with the same name from different modules will be distinct from each other. +with the same name defined in different modules will be distinct from each +other. Creating a copy of a sentinel object, such as by using ``copy.copy()`` or by pickling and unpickling, will return the same object. From aa00e7f84b71a75d26d50e35f222f67b1c645680 Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Mon, 27 Jan 2025 09:27:10 +0200 Subject: [PATCH 07/10] A few more words about the advantages of the bare name for type signatures --- peps/pep-0661.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/peps/pep-0661.rst b/peps/pep-0661.rst index 1481c55930e..72472e89247 100644 --- a/peps/pep-0661.rst +++ b/peps/pep-0661.rst @@ -409,8 +409,9 @@ This was suggested by several people in discussions and is what this PEP first went with. However, it was pointed out that this would cause potential confusion, due to e.g. ``Literal["MISSING"]`` referring to the string value ``"MISSING"`` rather than being a forward-reference to a sentinel value -``MISSING``. Using the bare name was also suggested often in discussions, and -follows the precedent and well-known pattern set by ``None``. +``MISSING``. Using the bare name was also suggested often in discussions. +This follows the precedent and well-known pattern set by ``None``, and has the +advantages of not requiring an import and being much shorter. Additional Notes From 89835277388a96affa2198a48fe36614caa972ae Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:13:45 +0200 Subject: [PATCH 08/10] Correct __reduce__ in the reference impl --- peps/pep-0661.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/peps/pep-0661.rst b/peps/pep-0661.rst index 72472e89247..a249d463738 100644 --- a/peps/pep-0661.rst +++ b/peps/pep-0661.rst @@ -286,7 +286,6 @@ simplified version follows:: self.__class__, ( self._name, - self._repr, self._module_name, ), ) From 353550313948f753d9c9ba0fdee40255e3684574 Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:14:57 +0200 Subject: [PATCH 09/10] Use the fully qualified name for the repr even when defined in a class --- peps/pep-0661.rst | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/peps/pep-0661.rst b/peps/pep-0661.rst index a249d463738..d0bdcfdcf7e 100644 --- a/peps/pep-0661.rst +++ b/peps/pep-0661.rst @@ -274,12 +274,11 @@ simplified version follows:: sentinel = super().__new__(cls) sentinel._name = name sentinel._module_name = module_name - sentinel._repr = name.split(".")[-1] return _registry.setdefault(registry_key, sentinel) def __repr__(self): - return self._repr + return self._name def __reduce__(self): return ( @@ -420,14 +419,13 @@ Additional Notes repo [7]_. * For sentinels defined in a class scope, to avoid potential name clashes, - one should use the fully-qualified name of the variable in the module. Only - the part of the name after the last period will be used for the default - repr. For example:: + one should use the fully-qualified name of the variable in the module. The + full name will be used as the repr. For example:: >>> class MyClass: ... NotGiven = sentinel('MyClass.NotGiven') >>> MyClass.NotGiven - + MyClass.NotGiven * One should be careful when creating sentinels in a function or method, since sentinels with the same name created by code in the same module will be From 0ab2f2f9ace5bf819197873ef531bf5e0b725dae Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:15:29 +0200 Subject: [PATCH 10/10] Remove custom repr in the example for the sentinel decorator suggestion --- peps/pep-0661.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0661.rst b/peps/pep-0661.rst index d0bdcfdcf7e..a13037d01f7 100644 --- a/peps/pep-0661.rst +++ b/peps/pep-0661.rst @@ -348,7 +348,7 @@ A sentinel class decorator The suggested idiom is:: - @sentinel(repr='') + @sentinel class NotGivenType: pass NotGiven = NotGivenType()