From d386fff31e3c00cd5680985c8a4ae9e2da002c5f Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:19:08 +0200 Subject: [PATCH] PEP 661: Some changes after submission (before SC review) (#4232) * Use "sentinellib" for the new module name This avoids breaking code using the existing "sentinels" PyPI package. * Change type annotations to use the bare name rather than Literal * Use sys._getframemodulename() instead of sys._getframe() in inline reference impl * Simplify, dropping support for custom repr and setting truthiness * Tweak some wording * A few more words about the advantages of the bare name for type signatures * Correct __reduce__ in the reference impl * Use the fully qualified name for the repr even when defined in a class * Remove custom repr in the example for the sentinel decorator suggestion --- peps/pep-0661.rst | 109 +++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 60 deletions(-) diff --git a/peps/pep-0661.rst b/peps/pep-0661.rst index 021b96430df..a13037d01f7 100644 --- a/peps/pep-0661.rst +++ b/peps/pep-0661.rst @@ -142,23 +142,12 @@ all of these criteria (see `Reference Implementation`_). Specification ============= -A new ``Sentinel`` class will be added to a new ``sentinels`` 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 - >>> NotGiven = Sentinel('NotGiven') - >>> NotGiven - - >>> MISSING = Sentinel('MISSING', repr='mymodule.MISSING') +A new ``Sentinel`` class will be added to a new ``sentinellib`` module. + + >>> from sentinellib import Sentinel + >>> 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 @@ -166,28 +155,29 @@ 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 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. -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. -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. @@ -240,17 +230,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 @@ -277,15 +257,12 @@ 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: - 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}' @@ -296,24 +273,18 @@ simplified version follows:: sentinel = super().__new__(cls) sentinel._name = name - sentinel._repr = repr - sentinel._bool_value = bool_value sentinel._module_name = module_name return _registry.setdefault(registry_key, sentinel) def __repr__(self): - return self._repr - - def __bool__(self): - return self._bool_value + return self._name def __reduce__(self): return ( self.__class__, ( self._name, - self._repr, self._module_name, ), ) @@ -377,7 +348,7 @@ A sentinel class decorator The suggested idiom is:: - @sentinel(repr='') + @sentinel class NotGivenType: pass NotGiven = NotGivenType() @@ -421,6 +392,26 @@ 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 +-------------------------------------------- + +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. +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 ================ @@ -428,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 @@ -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