-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
1 changed file
with
49 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
||
|
@@ -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='<NotGiven>') | ||
@sentinel | ||
class NotGivenType: pass | ||
NotGiven = NotGivenType() | ||
|
||
|
@@ -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 | ||
|
@@ -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 | ||
|