Skip to content

Commit

Permalink
pythongh-74929: PEP 667 general docs update (pythongh-119201)
Browse files Browse the repository at this point in the history
* expand on What's New entry for PEP 667 (including porting notes)
* define 'optimized scope' as a glossary term
* cover comprehensions and generator expressions in locals() docs
* review all mentions of "locals" in documentation (updating if needed)
* review all mentions of "f_locals" in documentation (updating if needed)
(cherry picked from commit e870c85)

Co-authored-by: Alyssa Coghlan <[email protected]>
  • Loading branch information
ncoghlan authored and miss-islington committed May 21, 2024
1 parent 24b0e8d commit c01fa1f
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 66 deletions.
11 changes: 6 additions & 5 deletions Doc/c-api/frame.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,18 @@ See also :ref:`Reflection <reflection>`.
.. c:function:: PyObject* PyFrame_GetLocals(PyFrameObject *frame)
Get the *frame*'s :attr:`~frame.f_locals` attribute.
If the frame refers to a function or comprehension, this returns
a write-through proxy object that allows modifying the locals.
In all other cases (classes, modules) it returns the :class:`dict`
representing the frame locals directly.
If the frame refers to an :term:`optimized scope`, this returns a
write-through proxy object that allows modifying the locals.
In all other cases (classes, modules, :func:`exec`, :func:`eval`) it returns
the mapping representing the frame locals directly (as described for
:func:`locals`).
Return a :term:`strong reference`.
.. versionadded:: 3.11
.. versionchanged:: 3.13
Return a proxy object for functions and comprehensions.
As part of :pep:`667`, return a proxy object for optimized scopes.
.. c:function:: int PyFrame_GetLineNumber(PyFrameObject *frame)
Expand Down
9 changes: 9 additions & 0 deletions Doc/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,15 @@ Glossary
(methods). Also the ultimate base class of any :term:`new-style
class`.

optimized scope
A scope where target local variable names are reliably known to the
compiler when the code is compiled, allowing optimization of read and
write access to these names. The local namespaces for functions,
generators, coroutines, comprehensions, and generator expressions are
optimized in this fashion. Note: most interpreter optimizations are
applied to all scopes, only those relying on a known set of local
and nonlocal variable names are restricted to optimized scopes.

package
A Python :term:`module` which can contain submodules or recursively,
subpackages. Technically, a package is a Python module with a
Expand Down
6 changes: 3 additions & 3 deletions Doc/library/code.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ build applications which provide an interactive interpreter prompt.
This class deals with parsing and interpreter state (the user's namespace); it
does not deal with input buffering or prompting or input file naming (the
filename is always passed in explicitly). The optional *locals* argument
specifies the dictionary in which code will be executed; it defaults to a newly
created dictionary with key ``'__name__'`` set to ``'__console__'`` and key
``'__doc__'`` set to ``None``.
specifies a mapping to use as the namespace in which code will be executed;
it defaults to a newly created dictionary with key ``'__name__'`` set to
``'__console__'`` and key ``'__doc__'`` set to ``None``.


.. class:: InteractiveConsole(locals=None, filename="<console>", local_exit=False)
Expand Down
103 changes: 68 additions & 35 deletions Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -543,18 +543,19 @@ are always available. They are listed here in alphabetical order.

The *expression* argument is parsed and evaluated as a Python expression
(technically speaking, a condition list) using the *globals* and *locals*
dictionaries as global and local namespace. If the *globals* dictionary is
mappings as global and local namespace. If the *globals* dictionary is
present and does not contain a value for the key ``__builtins__``, a
reference to the dictionary of the built-in module :mod:`builtins` is
inserted under that key before *expression* is parsed. That way you can
control what builtins are available to the executed code by inserting your
own ``__builtins__`` dictionary into *globals* before passing it to
:func:`eval`. If the *locals* dictionary is omitted it defaults to the
*globals* dictionary. If both dictionaries are omitted, the expression is
:func:`eval`. If the *locals* mapping is omitted it defaults to the
*globals* dictionary. If both mappings are omitted, the expression is
executed with the *globals* and *locals* in the environment where
:func:`eval` is called. Note, *eval()* does not have access to the
:func:`eval` is called. Note, *eval()* will only have access to the
:term:`nested scopes <nested scope>` (non-locals) in the enclosing
environment.
environment if they are already referenced in the scope that is calling
:func:`eval` (e.g. via a :keyword:`nonlocal` statement).

Example:

Expand Down Expand Up @@ -587,6 +588,11 @@ are always available. They are listed here in alphabetical order.

The *globals* and *locals* arguments can now be passed as keywords.

.. versionchanged:: 3.13

The semantics of the default *locals* namespace have been adjusted as
described for the :func:`locals` builtin.

.. index:: pair: built-in function; exec

.. function:: exec(source, /, globals=None, locals=None, *, closure=None)
Expand All @@ -612,9 +618,15 @@ are always available. They are listed here in alphabetical order.

.. note::

Most users should just pass a *globals* argument and never *locals*.
If exec gets two separate objects as *globals* and *locals*, the code
will be executed as if it were embedded in a class definition.
When ``exec`` gets two separate objects as *globals* and *locals*, the
code will be executed as if it were embedded in a class definition. This
means functions and classes defined in the executed code will not be able
to access variables assigned at the top level (as the "top level"
variables are treated as class variables in a class definition).
Passing a :class:`collections.ChainMap` instance as *globals* allows name
lookups to be chained across multiple mappings without triggering this
behaviour. Values assigned to top level names in the executed code can be
retrieved by passing an empty dictionary as the first entry in the chain.

If the *globals* dictionary does not contain a value for the key
``__builtins__``, a reference to the dictionary of the built-in module
Expand All @@ -635,7 +647,7 @@ are always available. They are listed here in alphabetical order.
.. note::

The built-in functions :func:`globals` and :func:`locals` return the current
global and local dictionary, respectively, which may be useful to pass around
global and local namespace, respectively, which may be useful to pass around
for use as the second and third argument to :func:`exec`.

.. note::
Expand All @@ -651,6 +663,11 @@ are always available. They are listed here in alphabetical order.

The *globals* and *locals* arguments can now be passed as keywords.

.. versionchanged:: 3.13

The semantics of the default *locals* namespace have been adjusted as
described for the :func:`locals` builtin.


.. function:: filter(function, iterable)

Expand Down Expand Up @@ -1056,39 +1073,51 @@ are always available. They are listed here in alphabetical order.
variable names as the keys, and their currently bound references as the
values.

At module scope, as well as when using ``exec()`` or ``eval()`` with a
single namespace, this function returns the same namespace as ``globals()``.
At module scope, as well as when using :func:`exec` or :func:`eval` with
a single namespace, this function returns the same namespace as
:func:`globals`.

At class scope, it returns the namespace that will be passed to the
metaclass constructor.

When using ``exec()`` or ``eval()`` with separate local and global
namespaces, it returns the local namespace passed in to the function call.
arguments, it returns the local namespace passed in to the function call.

In all of the above cases, each call to ``locals()`` in a given frame of
execution will return the *same* mapping object. Changes made through
the mapping object returned from ``locals()`` will be visible as bound,
rebound, or deleted local variables, and binding, rebinding, or deleting
local variables will immediately affect the contents of the returned mapping
object.

At function scope (including for generators and coroutines), each call to
``locals()`` instead returns a fresh dictionary containing the current
bindings of the function's local variables and any nonlocal cell references.
In this case, name binding changes made via the returned dict are *not*
written back to the corresponding local variables or nonlocal cell
references, and binding, rebinding, or deleting local variables and nonlocal
cell references does *not* affect the contents of previously returned
dictionaries.
the mapping object returned from ``locals()`` will be visible as assigned,
reassigned, or deleted local variables, and assigning, reassigning, or
deleting local variables will immediately affect the contents of the
returned mapping object.

In an :term:`optimized scope` (including functions, generators, and
coroutines), each call to ``locals()`` instead returns a fresh dictionary
containing the current bindings of the function's local variables and any
nonlocal cell references. In this case, name binding changes made via the
returned dict are *not* written back to the corresponding local variables
or nonlocal cell references, and assigning, reassigning, or deleting local
variables and nonlocal cell references does *not* affect the contents
of previously returned dictionaries.

Calling ``locals()`` as part of a comprehension in a function, generator, or
coroutine is equivalent to calling it in the containing scope, except that
the comprehension's initialised iteration variables will be included. In
other scopes, it behaves as if the comprehension were running as a nested
function.

Calling ``locals()`` as part of a generator expression is equivalent to
calling it in a nested generator function.

.. versionchanged:: 3.12
The behaviour of ``locals()`` in a comprehension has been updated as
described in :pep:`709`.

.. versionchanged:: 3.13
In previous versions, the semantics of mutating the mapping object
returned from this function were formally undefined. In CPython
specifically, the mapping returned at function scope could be
implicitly refreshed by other operations, such as calling ``locals()``
again. Obtaining the legacy CPython behaviour now requires explicit
calls to update the initially returned dictionary with the results
of subsequent calls to ``locals()``.
As part of :pep:`667`, the semantics of mutating the mapping objects
returned from this function are now defined. The behavior in
:term:`optimized scopes <optimized scope>` is now as described above.
Aside from being defined, the behaviour in other scopes remains
unchanged from previous versions.


.. function:: map(function, iterable, *iterables)
Expand Down Expand Up @@ -1975,14 +2004,18 @@ are always available. They are listed here in alphabetical order.
:attr:`~object.__dict__` attributes (for example, classes use a
:class:`types.MappingProxyType` to prevent direct dictionary updates).

Without an argument, :func:`vars` acts like :func:`locals`. Note, the
locals dictionary is only useful for reads since updates to the locals
dictionary are ignored.
Without an argument, :func:`vars` acts like :func:`locals`.

A :exc:`TypeError` exception is raised if an object is specified but
it doesn't have a :attr:`~object.__dict__` attribute (for example, if
its class defines the :attr:`~object.__slots__` attribute).

.. versionchanged:: 3.13

The result of calling this function without an argument has been
updated as described for the :func:`locals` builtin.


.. function:: zip(*iterables, strict=False)

Iterate over several iterables in parallel, producing tuples with an item
Expand Down
22 changes: 13 additions & 9 deletions Doc/library/pdb.rst
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ The typical usage to inspect a crashed program is::
0
(Pdb)

.. versionchanged:: 3.13
The implementation of :pep:`667` means that name assignments made via ``pdb``
will immediately affect the active scope, even when running inside an
:term:`optimized scope`.


The module defines the following functions; each enters the debugger in a
slightly different way:
Expand Down Expand Up @@ -579,18 +584,17 @@ can be overridden by the local file.

.. pdbcommand:: interact

Start an interactive interpreter (using the :mod:`code` module) whose global
namespace contains all the (global and local) names found in the current
scope. Use ``exit()`` or ``quit()`` to exit the interpreter and return to
the debugger.
Start an interactive interpreter (using the :mod:`code` module) in a new
global namespace initialised from the local and global namespaces for the
current scope. Use ``exit()`` or ``quit()`` to exit the interpreter and
return to the debugger.

.. note::

Because interact creates a new global namespace with the current global
and local namespace for execution, assignment to variables will not
affect the original namespaces.
However, modification to the mutable objects will be reflected in the
original namespaces.
As ``interact`` creates a new dedicated namespace for code execution,
assignments to variables will not affect the original namespaces.
However, modifications to any referenced mutable objects will be reflected
in the original namespaces as usual.

.. versionadded:: 3.2

Expand Down
2 changes: 1 addition & 1 deletion Doc/library/profile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ functions:
.. function:: runctx(command, globals, locals, filename=None, sort=-1)

This function is similar to :func:`run`, with added arguments to supply the
globals and locals dictionaries for the *command* string. This routine
globals and locals mappings for the *command* string. This routine
executes::

exec(command, globals, locals)
Expand Down
2 changes: 1 addition & 1 deletion Doc/library/traceback.rst
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ in a :ref:`traceback <traceback-objects>`.
attribute accessed (which also happens when casting it to a :class:`tuple`).
:attr:`~FrameSummary.line` may be directly provided, and will prevent line
lookups happening at all. *locals* is an optional local variable
dictionary, and if supplied the variable representations are stored in the
mapping, and if supplied the variable representations are stored in the
summary for later display.

:class:`!FrameSummary` instances have the following attributes:
Expand Down
2 changes: 1 addition & 1 deletion Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1346,7 +1346,7 @@ Special read-only attributes
* - .. attribute:: frame.f_locals
- The dictionary used by the frame to look up
:ref:`local variables <naming>`.
If the frame refers to a function or comprehension,
If the frame refers to an :term:`optimized scope`,
this may return a write-through proxy object.

.. versionchanged:: 3.13
Expand Down
55 changes: 51 additions & 4 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,11 @@ Interpreter improvements:
Performance improvements are modest -- we expect to be improving this
over the next few releases.

* :pep:`667`: :attr:`FrameType.f_locals <frame.f_locals>` when used in
a function now returns a write-through proxy to the frame's locals,
rather than a ``dict``. See the PEP for corresponding C API changes
and deprecations.
* :pep:`667`: The :func:`locals` builtin now has
:ref:`defined semantics <whatsnew313-locals-semantics>` when mutating the
returned mapping. Python debuggers and similar tools may now more reliably
update local variables in optimized frames even during concurrent code
execution.

New typing features:

Expand Down Expand Up @@ -247,6 +248,34 @@ Improved Error Messages
through ``self.X`` from any function in its body. (Contributed by Irit Katriel
in :gh:`115775`.)

.. _whatsnew313-locals-semantics:

Defined mutation semantics for ``locals()``
-------------------------------------------

Historically, the expected result of mutating the return value of :func:`locals`
has been left to individual Python implementations to define.

Through :pep:`667`, Python 3.13 standardises the historical behaviour of CPython
for most code execution scopes, but changes
:term:`optimized scopes <optimized scope>` (functions, generators, coroutines,
comprehensions, and generator expressions) to explicitly return independent
snapshots of the currently assigned local variables, including locally
referenced nonlocal variables captured in closures.

To ensure debuggers and similar tools can reliably update local variables in
scopes affected by this change, :attr:`FrameType.f_locals <frame.f_locals>` now
returns a write-through proxy to the frame's local and locally referenced
nonlocal variables in these scopes, rather than returning an inconsistently
updated shared ``dict`` instance with undefined runtime semantics.

See :pep:`667` for more details, including related C API changes and
deprecations.

(PEP and implementation contributed by Mark Shannon and Tian Gao in
:gh:`74929`. Documentation updates provided by Guido van Rossum and
Alyssa Coghlan.)

Incremental Garbage Collection
------------------------------

Expand Down Expand Up @@ -2177,6 +2206,24 @@ Changes in the Python API
returned by :meth:`zipfile.ZipFile.open` was changed from ``'r'`` to ``'rb'``.
(Contributed by Serhiy Storchaka in :gh:`115961`.)

* Calling :func:`locals` in an :term:`optimized scope` now produces an
independent snapshot on each call, and hence no longer implicitly updates
previously returned references. Obtaining the legacy CPython behaviour now
requires explicit calls to update the initially returned dictionary with the
results of subsequent calls to ``locals()``. (Changed as part of :pep:`667`.)

* Calling :func:`locals` from a comprehension at module or class scope
(including via ``exec`` or ``eval``) once more behaves as if the comprehension
were running as an independent nested function (i.e. the local variables from
the containing scope are not included). In Python 3.12, this had changed
to include the local variables from the containing scope when implementing
:pep:`709`. (Changed as part of :pep:`667`.)

* Accessing :attr:`FrameType.f_locals <frame.f_locals>` in an
:term:`optimized scope` now returns a write-through proxy rather than a
snapshot that gets updated at ill-specified times. If a snapshot is desired,
it must be created explicitly with ``dict`` or the proxy's ``.copy()`` method.
(Changed as part of :pep:`667`.)

Changes in the C API
--------------------
Expand Down
8 changes: 4 additions & 4 deletions Lib/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ class InteractiveInterpreter:
def __init__(self, locals=None):
"""Constructor.
The optional 'locals' argument specifies the dictionary in
which code will be executed; it defaults to a newly created
dictionary with key "__name__" set to "__console__" and key
"__doc__" set to None.
The optional 'locals' argument specifies a mapping to use as the
namespace in which code will be executed; it defaults to a newly
created dictionary with key "__name__" set to "__console__" and
key "__doc__" set to None.
"""
if locals is None:
Expand Down
9 changes: 6 additions & 3 deletions Lib/pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,9 +392,12 @@ def setup(self, f, tb):
self.tb_lineno[tb.tb_frame] = lineno
tb = tb.tb_next
self.curframe = self.stack[self.curindex][0]
# The f_locals dictionary is updated from the actual frame
# locals whenever the .f_locals accessor is called, so we
# cache it here to ensure that modifications are not overwritten.
# The f_locals dictionary used to be updated from the actual frame
# locals whenever the .f_locals accessor was called, so it was
# cached here to ensure that modifications were not overwritten. While
# the caching is no longer required now that f_locals is a direct proxy
# on optimized frames, it's also harmless, so the code structure has
# been left unchanged.
self.curframe_locals = self.curframe.f_locals
self.set_convenience_variable(self.curframe, '_frame', self.curframe)

Expand Down

0 comments on commit c01fa1f

Please sign in to comment.