PEP 505 updates (#747)

Improve readability of PEP 505
This commit is contained in:
Steve Dower 2018-07-23 07:17:15 -07:00 committed by GitHub
parent ce5590c1c1
commit 3bdd70578a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 189 additions and 49 deletions

View File

@ -33,10 +33,18 @@ definitions and other language's implementations of those above. Specifically:
if it evaluates to a value that is not ``None``, or else it evaluates and if it evaluates to a value that is not ``None``, or else it evaluates and
returns the right hand side. A coalescing ``??=`` augmented assignment returns the right hand side. A coalescing ``??=`` augmented assignment
operator is included. operator is included.
* The "``None``-aware attribute access" operator ``?.`` evaluates the complete * The "``None``-aware attribute access" operator ``?.`` ("maybe dot") evaluates
expression if the left hand side evaluates to a value that is not ``None`` the complete expression if the left hand side evaluates to a value that is
* The "``None``-aware indexing" operator ``?[]`` evaluates the complete not ``None``
expression if the left hand site evaluates to a value that is not ``None`` * The "``None``-aware indexing" operator ``?[]`` ("maybe subscript") evaluates
the complete expression if the left hand site evaluates to a value that is
not ``None``
See the `Grammar changes`_ section for specifics and examples of the required
grammar changes.
See the `Examples`_ section for more realistic examples of code that could be
updated to use the new operators.
Syntax and Semantics Syntax and Semantics
==================== ====================
@ -48,7 +56,7 @@ The ``None`` object denotes the lack of a value. For the purposes of these
operators, the lack of a value indicates that the remainder of the expression operators, the lack of a value indicates that the remainder of the expression
also lacks a value and should not be evaluated. also lacks a value and should not be evaluated.
A rejected proposal was to treat any value that evaluates to false in a A rejected proposal was to treat any value that evaluates as "false" in a
Boolean context as not having a value. However, the purpose of these operators Boolean context as not having a value. However, the purpose of these operators
is to propagate the "lack of value" state, rather than the "false" state. is to propagate the "lack of value" state, rather than the "false" state.
@ -56,7 +64,7 @@ Some argue that this makes ``None`` special. We contend that ``None`` is
already special, and that using it as both the test and the result of these already special, and that using it as both the test and the result of these
operators does not change the existing semantics in any way. operators does not change the existing semantics in any way.
See the `Rejected Ideas`_ section for discussion on the rejected approaches. See the `Rejected Ideas`_ section for discussions on alternate approaches.
Grammar changes Grammar changes
--------------- ---------------
@ -75,11 +83,21 @@ The following rules of the Python grammar are updated to read::
'.' NAME | '.' NAME |
'?.' NAME) '?.' NAME)
Inserting the ``coalesce`` rule in this location ensures that expressions The coalesce rule
resulting in ``None`` are natuarlly coalesced before they are used in *****************
operations that would typically raise ``TypeError``. Like ``and`` and ``or``
the right-hand expression is not evaluated until the left-hand side is The ``coalesce`` rule provides the ``??`` binary operator. Unlike most binary
determined to be ``None``. For example:: operators, the right-hand side is not evaulated until the left-hand side is
determined to be ``None``.
The ``??`` operator binds more tightly than other binary operators as most
existing implementations of these do not propagate ``None``s (they will
typically raise ``TypeError``). Expressions that are known to potentially
result in ``None`` can be substituted for a default value without needing
additional parentheses.
Some examples of how implicit parentheses are placed when evaluating operator
precedence in the presence of the ``??`` operator.
a, b = None, None a, b = None, None
def c(): return None def c(): return None
@ -92,9 +110,17 @@ determined to be ``None``. For example::
(True ?? ex()) == True (True ?? ex()) == True
(c ?? ex)() == c() (c ?? ex)() == c()
Augmented coalescing assignment only rebinds the name if its current value is Particularly for cases such as ``a ?? 2 ** b ?? 3``, parenthesizing the
``None``. If the target name already has a value, the right-hand side is not sub-expressions any other way would result in ``TypeError``, as ``int.__pow__``
evaluated. For example:: cannot be called with ``None`` (and the fact that the ``??`` operator is used
at all implies that ``a`` or ``b`` may be ``None``). However, as usual,
while parentheses are not required they should be added if it helps improve
readability.
An augmented assignment for the ``??`` operator is also added. Augmented
coalescing assignment only rebinds the name if its current value is ``None``.
If the target name already has a value, the right-hand side is not evaluated.
For example::
a = None a = None
b = '' b = ''
@ -106,13 +132,16 @@ evaluated. For example::
assert a == 'value' assert a == 'value'
assert b == '' assert b == ''
assert c == '0' and any(os.scandir('/')) assert c == 0 and any(os.scandir('/'))
Adding new trailers for the other ``None``-aware operators ensures that they The maybe-dot and maybe-subscript operators
may be used in all valid locations for the existing equivalent operators, *******************************************
including as part of an assignment target (more details below). As the existing
evaluation rules are not directly embedded in the grammar, we specify the The maybe-dot and maybe-subscript operators are added as trailers for atoms,
required changes here. so that they may be used in all the same locations as the regular operators,
including as part of an assignment target (more details below). As the
existing evaluation rules are not directly embedded in the grammar, we specify
the required changes below.
Assume that the ``atom`` is always successfully evaluated. Each ``trailer`` is Assume that the ``atom`` is always successfully evaluated. Each ``trailer`` is
then evaluated from left to right, applying its own parameter (either its then evaluated from left to right, applying its own parameter (either its
@ -174,6 +203,28 @@ though unlikely to be useful unless combined with a coalescing operation::
(a?.b ?? d).c = 1 (a?.b ?? d).c = 1
Reading expressions
-------------------
For the maybe-dot and maybe-subscript operators, the intention is that
expressions including these operators should be read and interpreted as for the
regular versions of these operators. In "normal" cases, the end results are
going to be identical between an expression such as ``a?.b?[c]`` and
``a.b[c]``, and just as we do not currently read "a.b" as "read attribute b
from a *if it has an attribute a or else it raises AttributeError*", there is
no need to read "a?.b" as "read attribute b from a *if a is not None*"
(unless in a context where the listener needs to be aware of the specific
behaviour).
For coalescing expressions using the ``??`` operator, expressions should either
be read as "or ... if None" or "coalesced with". For example, the expression
``a.get_value() ?? 100`` would be read "call a dot get_value or 100 if None",
or "call a dot get_value coalesced with 100".
.. note::
Reading code in spoken text is always lossy, and so we make no attempt to
define an unambiguous way of speaking these operators. These suggestions
are intended to add context to the implications of adding the new syntax.
Examples Examples
======== ========
@ -227,35 +278,67 @@ After updating to use the ``??`` operator::
optdict = dict(encoding=encoding ?? sys.getdefaultencoding(), optdict = dict(encoding=encoding ?? sys.getdefaultencoding(),
css=options.css) css=options.css)
From ``dis.py``:: From ``email/generator.py`` (and importantly note that there is no way to
substitute ``or`` for ``??`` in this situation)::
def _get_const_info(const_index, const_list): mangle_from_ = True if policy is None else policy.mangle_from_
argval = const_index
if const_list is not None:
argval = const_list[const_index]
return argval, repr(argval)
After updating to use the ``?[]`` and ``??`` operators:: After updating::
def _get_const_info(const_index, const_list): mangle_from_ = policy?.mangle_from_ ?? True
argval = const_list?[const_index] ?? const_index
return argval, repr(argval)
From ``inspect.py``::
for base in object.__bases__: From ``asyncio/subprocess.py``::
for name in getattr(base, "__abstractmethods__", ()):
value = getattr(object, name, None)
if getattr(value, "__isabstractmethod__", False):
return True
After updating to use the ``?.`` operator (and deliberately not converting to def pipe_data_received(self, fd, data):
use ``any()``):: if fd == 1:
reader = self.stdout
elif fd == 2:
reader = self.stderr
else:
reader = None
if reader is not None:
reader.feed_data(data)
After updating to use the ``?.`` operator::
def pipe_data_received(self, fd, data):
if fd == 1:
reader = self.stdout
elif fd == 2:
reader = self.stderr
else:
reader = None
reader?.feed_data(data)
From ``asyncio/tasks.py``::
try:
await waiter
finally:
if timeout_handle is not None:
timeout_handle.cancel()
After updating to use the ``?.`` operator::
try:
await waiter
finally:
timeout_handle?.cancel()
From ``ctypes/_aix.py``::
if libpaths is None:
libpaths = []
else:
libpaths = libpaths.split(":")
After updating::
libpaths = libpaths?.split(":") ?? []
for base in object.__bases__:
for name in base?.__abstractmethods__ ?? ():
if object?.name?.__isabstractmethod__:
return True
From ``os.py``:: From ``os.py``::
@ -275,6 +358,43 @@ After updating to use the ``?.`` operator::
nondirs.append(name) nondirs.append(name)
From ``importlib/abc.py``::
def find_module(self, fullname, path):
if not hasattr(self, 'find_spec'):
return None
found = self.find_spec(fullname, path)
return found.loader if found is not None else None
After partially updating::
def find_module(self, fullname, path):
if not hasattr(self, 'find_spec'):
return None
return self.find_spec(fullname, path)?.loader
After extensive updating (arguably excessive, though that's for the style
guides to determine)::
def find_module(self, fullname, path):
return getattr(self, 'find_spec', None)?.__call__(fullname, path)?.loader
From ``dis.py``::
def _get_const_info(const_index, const_list):
argval = const_index
if const_list is not None:
argval = const_list[const_index]
return argval, repr(argval)
After updating to use the ``?[]`` and ``??`` operators::
def _get_const_info(const_index, const_list):
argval = const_list?[const_index] ?? const_index
return argval, repr(argval)
jsonify jsonify
------- -------

View File

@ -8,7 +8,10 @@ Example usage:
''' '''
import ast import ast
import glob
import os
import sys import sys
import tokenize
class NoneCoalesceIfBlockVisitor(ast.NodeVisitor): class NoneCoalesceIfBlockVisitor(ast.NodeVisitor):
@ -379,15 +382,16 @@ def log(text, file_, start_line, stop_line=None):
if stop_line is None: if stop_line is None:
stop_line = start_line stop_line = start_line
with open(file_) as source: try:
source = tokenize.open(file_)
except (SyntaxError, UnicodeDecodeError):
return
with source:
print(''.join(source.readlines()[start_line-1:stop_line])) print(''.join(source.readlines()[start_line-1:stop_line]))
def main(): def main():
if len(sys.argv) < 2:
sys.stderr.write('Usage: python3 parse.py <files>\n')
sys.exit(1)
def make_callback(text): def make_callback(text):
return count_calls_decorator( return count_calls_decorator(
lambda file_, start, stop: log(text, file_, start, stop) lambda file_, start, stop: log(text, file_, start, stop)
@ -400,8 +404,24 @@ def main():
sni_callback = make_callback('Safe navigation `if` block') sni_callback = make_callback('Safe navigation `if` block')
snt_callback = make_callback('Safe navigation ternary') snt_callback = make_callback('Safe navigation ternary')
for file_ in sys.argv[1:]: files = sys.argv[1:]
with open(file_) as source: if files:
expanded_files = []
for file_ in files:
if '*' in file_:
expanded_files.extend(glob.glob(file_))
else:
expanded_files.append(file_)
else:
files = glob.glob(os.path.join(sys.prefix, 'Lib', '**', '*.py'))
for file_ in files:
try:
source = tokenize.open(file_)
except (SyntaxError, UnicodeDecodeError):
continue
with source:
try: try:
tree = ast.parse(source.read(), filename=file_) tree = ast.parse(source.read(), filename=file_)
except SyntaxError: except SyntaxError: