Skip to content

Commit

Permalink
PEP 661: Some changes after submission (before SC review) (#4232)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
taleinat authored Jan 27, 2025
1 parent 24e1e4a commit d386fff
Showing 1 changed file with 49 additions and 60 deletions.
109 changes: 49 additions & 60 deletions peps/pep-0661.rst
Original file line number Diff line number Diff line change
Expand Up @@ -142,52 +142,42 @@ 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
<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='<MEGA>',
bool_value=False,
module_name='mymodule')
<MEGA>
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
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.

Expand Down Expand Up @@ -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
Expand All @@ -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}'
Expand All @@ -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,
),
)
Expand Down Expand Up @@ -377,7 +348,7 @@ A sentinel class decorator

The suggested idiom is::

@sentinel(repr='<NotGiven>')
@sentinel
class NotGivenType: pass
NotGiven = NotGivenType()

Expand Down Expand Up @@ -421,21 +392,40 @@ 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
================

* This PEP and the initial implementation are drafted in a dedicated GitHub
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
<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
Expand Down Expand Up @@ -482,7 +472,6 @@ Footnotes
.. [7] `Reference implementation at the taleinat/python-stdlib-sentinels GitHub repo <https://github.com/taleinat/python-stdlib-sentinels>`_
.. [8] `bpo-35712: Make NotImplemented unusable in boolean context <https://github.com/python/cpython/issues/79893>`_
.. [9] `Discussion thread about type signatures for these sentinels on the typing-sig mailing list <https://mail.python.org/archives/list/[email protected]/thread/NDEJ7UCDPINP634GXWDARVMTGDVSNBKV/#LVCPTY26JQJW7NKGKGAZXHQKWVW7GOGL>`_
.. [10] `sentinels package on PyPI <https://pypi.org/project/sentinels/>`_
Copyright
Expand Down

0 comments on commit d386fff

Please sign in to comment.