diff --git a/pep-0505.rst b/pep-0505.rst index 45a0ef73c..d76399e27 100644 --- a/pep-0505.rst +++ b/pep-0505.rst @@ -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 returns the right hand side. A coalescing ``??=`` augmented assignment operator is included. -* The "``None``-aware attribute access" operator ``?.`` evaluates the complete - expression if the left hand side evaluates to a value that is not ``None`` -* The "``None``-aware indexing" operator ``?[]`` evaluates the complete - expression if the left hand site evaluates to a value that is not ``None`` +* The "``None``-aware attribute access" operator ``?.`` ("maybe dot") evaluates + the complete expression if the left hand side 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 ==================== @@ -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 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 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 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 --------------- @@ -75,11 +83,21 @@ The following rules of the Python grammar are updated to read:: '.' NAME | '?.' NAME) -Inserting the ``coalesce`` rule in this location ensures that expressions -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 -determined to be ``None``. For example:: +The coalesce rule +***************** + +The ``coalesce`` rule provides the ``??`` binary operator. Unlike most binary +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 def c(): return None @@ -92,9 +110,17 @@ determined to be ``None``. For example:: (True ?? ex()) == True (c ?? ex)() == c() -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:: +Particularly for cases such as ``a ?? 2 ** b ?? 3``, parenthesizing the +sub-expressions any other way would result in ``TypeError``, as ``int.__pow__`` +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 b = '' @@ -106,13 +132,16 @@ evaluated. For example:: assert a == 'value' 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 -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 -required changes here. +The maybe-dot and maybe-subscript operators +******************************************* + +The maybe-dot and maybe-subscript operators are added as trailers for atoms, +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 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 +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 ======== @@ -227,35 +278,67 @@ After updating to use the ``??`` operator:: optdict = dict(encoding=encoding ?? sys.getdefaultencoding(), 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): - argval = const_index - if const_list is not None: - argval = const_list[const_index] - return argval, repr(argval) + mangle_from_ = True if policy is None else policy.mangle_from_ -After updating to use the ``?[]`` and ``??`` operators:: +After updating:: - def _get_const_info(const_index, const_list): - argval = const_list?[const_index] ?? const_index - return argval, repr(argval) + mangle_from_ = policy?.mangle_from_ ?? True -From ``inspect.py``:: - for base in object.__bases__: - for name in getattr(base, "__abstractmethods__", ()): - value = getattr(object, name, None) - if getattr(value, "__isabstractmethod__", False): - return True +From ``asyncio/subprocess.py``:: -After updating to use the ``?.`` operator (and deliberately not converting to -use ``any()``):: + def pipe_data_received(self, fd, data): + 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``:: @@ -275,6 +358,43 @@ After updating to use the ``?.`` operator:: 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 ------- diff --git a/pep-0505/find-pep505.py b/pep-0505/find-pep505.py index 828e98715..2c87da37a 100644 --- a/pep-0505/find-pep505.py +++ b/pep-0505/find-pep505.py @@ -8,7 +8,10 @@ Example usage: ''' import ast +import glob +import os import sys +import tokenize class NoneCoalesceIfBlockVisitor(ast.NodeVisitor): @@ -379,15 +382,16 @@ def log(text, file_, start_line, stop_line=None): if stop_line is None: 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])) def main(): - if len(sys.argv) < 2: - sys.stderr.write('Usage: python3 parse.py \n') - sys.exit(1) - def make_callback(text): return count_calls_decorator( lambda file_, start, stop: log(text, file_, start, stop) @@ -400,8 +404,24 @@ def main(): sni_callback = make_callback('Safe navigation `if` block') snt_callback = make_callback('Safe navigation ternary') - for file_ in sys.argv[1:]: - with open(file_) as source: + files = sys.argv[1:] + 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: tree = ast.parse(source.read(), filename=file_) except SyntaxError: