2017-12-12 12:11:31 -05:00
|
|
|
PEP: 567
|
|
|
|
Title: Context Variables
|
|
|
|
Version: $Revision$
|
|
|
|
Last-Modified: $Date$
|
2019-09-12 07:36:34 -04:00
|
|
|
Author: Yury Selivanov <yury@edgedb.com>
|
2018-01-22 20:11:15 -05:00
|
|
|
Status: Final
|
2017-12-12 12:11:31 -05:00
|
|
|
Type: Standards Track
|
|
|
|
Content-Type: text/x-rst
|
|
|
|
Created: 12-Dec-2017
|
|
|
|
Python-Version: 3.7
|
2018-01-17 08:54:57 -05:00
|
|
|
Post-History: 12-Dec-2017, 28-Dec-2017, 16-Jan-2018
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
|
|
|
|
Abstract
|
|
|
|
========
|
|
|
|
|
2017-12-12 20:03:42 -05:00
|
|
|
This PEP proposes a new ``contextvars`` module and a set of new
|
2017-12-12 12:11:31 -05:00
|
|
|
CPython C APIs to support context variables. This concept is
|
2017-12-13 17:06:41 -05:00
|
|
|
similar to thread-local storage (TLS), but, unlike TLS, it also allows
|
2017-12-12 12:11:31 -05:00
|
|
|
correctly keeping track of values per asynchronous task, e.g.
|
|
|
|
``asyncio.Task``.
|
|
|
|
|
2017-12-12 23:03:05 -05:00
|
|
|
This proposal is a simplified version of :pep:`550`. The key
|
|
|
|
difference is that this PEP is concerned only with solving the case
|
|
|
|
for asynchronous tasks, not for generators. There are no proposed
|
|
|
|
modifications to any built-in types or to the interpreter.
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2017-12-28 01:01:05 -05:00
|
|
|
This proposal is not strictly related to Python Context Managers.
|
|
|
|
Although it does provide a mechanism that can be used by Context
|
|
|
|
Managers to store their state.
|
|
|
|
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-09-21 15:37:11 -04:00
|
|
|
API Design and Implementation Revisions
|
|
|
|
=======================================
|
|
|
|
|
|
|
|
In **Python 3.7.1** the signatures of all context variables
|
|
|
|
C APIs were **changed** to use ``PyObject *`` pointers instead
|
|
|
|
of ``PyContext *``, ``PyContextVar *``, and ``PyContextToken *``,
|
|
|
|
e.g.::
|
|
|
|
|
|
|
|
// in 3.7.0:
|
|
|
|
PyContext *PyContext_New(void);
|
|
|
|
|
|
|
|
// in 3.7.1+:
|
|
|
|
PyObject *PyContext_New(void);
|
|
|
|
|
|
|
|
See [6]_ for more details. The `C API`_ section of this PEP was
|
|
|
|
updated to reflect the change.
|
|
|
|
|
|
|
|
|
2017-12-12 12:11:31 -05:00
|
|
|
Rationale
|
|
|
|
=========
|
|
|
|
|
2017-12-12 22:40:33 -05:00
|
|
|
Thread-local variables are insufficient for asynchronous tasks that
|
2017-12-12 12:11:31 -05:00
|
|
|
execute concurrently in the same OS thread. Any context manager that
|
2017-12-12 20:03:42 -05:00
|
|
|
saves and restores a context value using ``threading.local()`` will
|
|
|
|
have its context values bleed to other code unexpectedly when used
|
|
|
|
in async/await code.
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
A few examples where having a working context local storage for
|
2017-12-12 20:03:42 -05:00
|
|
|
asynchronous code is desirable:
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2017-12-12 22:40:33 -05:00
|
|
|
* Context managers like ``decimal`` contexts and ``numpy.errstate``.
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
* Request-related data, such as security tokens and request
|
2017-12-12 20:03:42 -05:00
|
|
|
data in web applications, language context for ``gettext``, etc.
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
* Profiling, tracing, and logging in large code bases.
|
|
|
|
|
|
|
|
|
|
|
|
Introduction
|
|
|
|
============
|
|
|
|
|
|
|
|
The PEP proposes a new mechanism for managing context variables.
|
|
|
|
The key classes involved in this mechanism are ``contextvars.Context``
|
|
|
|
and ``contextvars.ContextVar``. The PEP also proposes some policies
|
|
|
|
for using the mechanism around asynchronous tasks.
|
|
|
|
|
|
|
|
The proposed mechanism for accessing context variables uses the
|
2017-12-12 20:03:42 -05:00
|
|
|
``ContextVar`` class. A module (such as ``decimal``) that wishes to
|
2018-01-17 08:54:57 -05:00
|
|
|
use the new mechanism should:
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
* declare a module-global variable holding a ``ContextVar`` to
|
2017-12-12 22:40:33 -05:00
|
|
|
serve as a key;
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
* access the current value via the ``get()`` method on the
|
|
|
|
key variable;
|
|
|
|
|
|
|
|
* modify the current value via the ``set()`` method on the
|
|
|
|
key variable.
|
|
|
|
|
|
|
|
The notion of "current value" deserves special consideration:
|
|
|
|
different asynchronous tasks that exist and execute concurrently
|
2021-02-03 09:06:23 -05:00
|
|
|
may have different values for the same key. This idea is well known
|
2017-12-12 22:40:33 -05:00
|
|
|
from thread-local storage but in this case the locality of the value is
|
|
|
|
not necessarily bound to a thread. Instead, there is the notion of the
|
2018-01-17 08:54:57 -05:00
|
|
|
"current ``Context``" which is stored in thread-local storage.
|
|
|
|
Manipulation of the current context is the responsibility of the
|
2017-12-12 12:11:31 -05:00
|
|
|
task framework, e.g. asyncio.
|
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
A ``Context`` is a mapping of ``ContextVar`` objects to their values.
|
|
|
|
The ``Context`` itself exposes the ``abc.Mapping`` interface
|
|
|
|
(not ``abc.MutableMapping``!), so it cannot be modified directly.
|
|
|
|
To set a new value for a context variable in a ``Context`` object,
|
|
|
|
the user needs to:
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
* make the ``Context`` object "current" using the ``Context.run()``
|
|
|
|
method;
|
|
|
|
|
|
|
|
* use ``ContextVar.set()`` to set a new value for the context
|
|
|
|
variable.
|
|
|
|
|
|
|
|
The ``ContextVar.get()`` method looks for the variable in the current
|
|
|
|
``Context`` object using ``self`` as a key.
|
|
|
|
|
|
|
|
It is not possible to get a direct reference to the current ``Context``
|
|
|
|
object, but it is possible to obtain a shallow copy of it using the
|
|
|
|
``contextvars.copy_context()`` function. This ensures that the
|
|
|
|
*caller* of ``Context.run()`` is the sole owner of its ``Context``
|
|
|
|
object.
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
|
|
|
|
Specification
|
|
|
|
=============
|
|
|
|
|
|
|
|
A new standard library module ``contextvars`` is added with the
|
|
|
|
following APIs:
|
|
|
|
|
2018-01-17 11:13:59 -05:00
|
|
|
1. The ``copy_context() -> Context`` function is used to get a copy of
|
2017-12-28 01:01:05 -05:00
|
|
|
the current ``Context`` object for the current OS thread.
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 11:13:59 -05:00
|
|
|
2. The ``ContextVar`` class to declare and access context variables.
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 11:13:59 -05:00
|
|
|
3. The ``Context`` class encapsulates context state. Every OS thread
|
2017-12-12 12:11:31 -05:00
|
|
|
stores a reference to its current ``Context`` instance.
|
2018-01-17 08:54:57 -05:00
|
|
|
It is not possible to control that reference directly.
|
2017-12-12 23:03:05 -05:00
|
|
|
Instead, the ``Context.run(callable, *args, **kwargs)`` method is
|
|
|
|
used to run Python code in another context.
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
|
|
|
|
contextvars.ContextVar
|
|
|
|
----------------------
|
|
|
|
|
|
|
|
The ``ContextVar`` class has the following constructor signature:
|
2017-12-12 23:03:05 -05:00
|
|
|
``ContextVar(name, *, default=_NO_DEFAULT)``. The ``name`` parameter
|
2018-01-17 08:54:57 -05:00
|
|
|
is used for introspection and debug purposes, and is exposed
|
2017-12-12 23:03:05 -05:00
|
|
|
as a read-only ``ContextVar.name`` attribute. The ``default``
|
2017-12-12 12:11:31 -05:00
|
|
|
parameter is optional. Example::
|
|
|
|
|
|
|
|
# Declare a context variable 'var' with the default value 42.
|
|
|
|
var = ContextVar('var', default=42)
|
|
|
|
|
2017-12-12 23:03:05 -05:00
|
|
|
(The ``_NO_DEFAULT`` is an internal sentinel object used to
|
|
|
|
detect if the default value was provided.)
|
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
``ContextVar.get(default=_NO_DEFAULT)`` returns a value for
|
|
|
|
the context variable for the current ``Context``::
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
# Get the value of `var`.
|
|
|
|
var.get()
|
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
If there is no value for the variable in the current context,
|
|
|
|
``ContextVar.get()`` will:
|
|
|
|
|
|
|
|
* return the value of the *default* argument of the ``get()`` method,
|
|
|
|
if provided; or
|
|
|
|
|
|
|
|
* return the default value for the context variable, if provided; or
|
|
|
|
|
|
|
|
* raise a ``LookupError``.
|
|
|
|
|
2017-12-12 12:11:31 -05:00
|
|
|
``ContextVar.set(value) -> Token`` is used to set a new value for
|
|
|
|
the context variable in the current ``Context``::
|
|
|
|
|
|
|
|
# Set the variable 'var' to 1 in the current context.
|
|
|
|
var.set(1)
|
|
|
|
|
2017-12-12 23:42:13 -05:00
|
|
|
``ContextVar.reset(token)`` is used to reset the variable in the
|
|
|
|
current context to the value it had before the ``set()`` operation
|
2018-01-17 08:54:57 -05:00
|
|
|
that created the ``token`` (or to remove the variable if it was
|
|
|
|
not set)::
|
2017-12-12 23:42:13 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
# Assume: var.get(None) is None
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
# Set 'var' to 1:
|
2017-12-12 20:41:25 -05:00
|
|
|
token = var.set(1)
|
2017-12-12 12:11:31 -05:00
|
|
|
try:
|
2018-01-17 08:54:57 -05:00
|
|
|
# var.get() == 1
|
2017-12-12 12:11:31 -05:00
|
|
|
finally:
|
2017-12-12 20:41:25 -05:00
|
|
|
var.reset(token)
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
# After reset: var.get(None) is None,
|
|
|
|
# i.e. 'var' was removed from the current context.
|
2017-12-12 23:42:13 -05:00
|
|
|
|
2018-01-17 12:42:29 -05:00
|
|
|
The ``ContextVar.reset()`` method raises:
|
2018-01-17 08:54:57 -05:00
|
|
|
|
2018-01-17 12:42:29 -05:00
|
|
|
* a ``ValueError`` if it is called with a token object created
|
|
|
|
by another variable;
|
2018-01-17 08:54:57 -05:00
|
|
|
|
2018-01-17 12:42:29 -05:00
|
|
|
* a ``ValueError`` if the current ``Context`` object does not match
|
|
|
|
the one where the token object was created;
|
|
|
|
|
|
|
|
* a ``RuntimeError`` if the token object has already been used once
|
|
|
|
to reset the variable.
|
2017-12-12 23:03:05 -05:00
|
|
|
|
2017-12-12 23:42:13 -05:00
|
|
|
|
|
|
|
contextvars.Token
|
|
|
|
-----------------
|
|
|
|
|
|
|
|
``contextvars.Token`` is an opaque object that should be used to
|
2018-01-17 08:54:57 -05:00
|
|
|
restore the ``ContextVar`` to its previous value, or to remove it from
|
2017-12-12 23:42:13 -05:00
|
|
|
the context if the variable was not set before. It can be created
|
|
|
|
only by calling ``ContextVar.set()``.
|
|
|
|
|
2017-12-13 12:25:17 -05:00
|
|
|
For debug and introspection purposes it has:
|
|
|
|
|
|
|
|
* a read-only attribute ``Token.var`` pointing to the variable
|
|
|
|
that created the token;
|
|
|
|
|
|
|
|
* a read-only attribute ``Token.old_value`` set to the value the
|
|
|
|
variable had before the ``set()`` call, or to ``Token.MISSING``
|
|
|
|
if the variable wasn't set before.
|
2017-12-12 23:42:13 -05:00
|
|
|
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
contextvars.Context
|
|
|
|
-------------------
|
|
|
|
|
2017-12-12 20:41:25 -05:00
|
|
|
``Context`` object is a mapping of context variables to values.
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2017-12-28 01:01:05 -05:00
|
|
|
``Context()`` creates an empty context. To get a copy of the current
|
|
|
|
``Context`` for the current OS thread, use the
|
|
|
|
``contextvars.copy_context()`` method::
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2017-12-28 01:01:05 -05:00
|
|
|
ctx = contextvars.copy_context()
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
To run Python code in some ``Context``, use ``Context.run()``
|
|
|
|
method::
|
|
|
|
|
|
|
|
ctx.run(function)
|
|
|
|
|
2017-12-12 20:03:42 -05:00
|
|
|
Any changes to any context variables that ``function`` causes will
|
2017-12-12 12:11:31 -05:00
|
|
|
be contained in the ``ctx`` context::
|
|
|
|
|
|
|
|
var = ContextVar('var')
|
|
|
|
var.set('spam')
|
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
def main():
|
|
|
|
# 'var' was set to 'spam' before
|
|
|
|
# calling 'copy_context()' and 'ctx.run(main)', so:
|
|
|
|
# var.get() == ctx[var] == 'spam'
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
var.set('ham')
|
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
# Now, after setting 'var' to 'ham':
|
|
|
|
# var.get() == ctx[var] == 'ham'
|
2017-12-12 20:41:25 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
ctx = copy_context()
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
# Any changes that the 'main' function makes to 'var'
|
|
|
|
# will be contained in 'ctx'.
|
|
|
|
ctx.run(main)
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
# The 'main()' function was run in the 'ctx' context,
|
|
|
|
# so changes to 'var' are contained in it:
|
|
|
|
# ctx[var] == 'ham'
|
2017-12-12 23:03:05 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
# However, outside of 'ctx', 'var' is still set to 'spam':
|
|
|
|
# var.get() == 'spam'
|
2017-12-12 23:03:05 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
``Context.run()`` raises a ``RuntimeError`` when called on the same
|
|
|
|
context object from more than one OS thread, or when called
|
|
|
|
recursively.
|
2017-12-12 23:03:05 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
``Context.copy()`` returns a shallow copy of the context object.
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
``Context`` objects implement the ``collections.abc.Mapping`` ABC.
|
2018-01-17 08:54:57 -05:00
|
|
|
This can be used to introspect contexts::
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2017-12-28 01:01:05 -05:00
|
|
|
ctx = contextvars.copy_context()
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2017-12-12 23:03:05 -05:00
|
|
|
# Print all context variables and their values in 'ctx':
|
2017-12-12 12:11:31 -05:00
|
|
|
print(ctx.items())
|
|
|
|
|
|
|
|
# Print the value of 'some_variable' in context 'ctx':
|
|
|
|
print(ctx[some_variable])
|
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
Note that all Mapping methods, including ``Context.__getitem__`` and
|
|
|
|
``Context.get``, ignore default values for context variables
|
|
|
|
(i.e. ``ContextVar.default``). This means that for a variable *var*
|
|
|
|
that was created with a default value and was not set in the
|
|
|
|
*context*:
|
|
|
|
|
2018-01-17 22:59:27 -05:00
|
|
|
* ``context[var]`` raises a ``KeyError``,
|
2018-01-17 08:54:57 -05:00
|
|
|
|
2018-01-17 22:59:27 -05:00
|
|
|
* ``var in context`` returns ``False``,
|
2018-01-17 08:54:57 -05:00
|
|
|
|
2018-01-17 22:59:27 -05:00
|
|
|
* the variable isn't included in ``context.items()``, etc.
|
2018-01-17 08:54:57 -05:00
|
|
|
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
asyncio
|
|
|
|
-------
|
|
|
|
|
|
|
|
``asyncio`` uses ``Loop.call_soon()``, ``Loop.call_later()``,
|
|
|
|
and ``Loop.call_at()`` to schedule the asynchronous execution of a
|
|
|
|
function. ``asyncio.Task`` uses ``call_soon()`` to run the
|
|
|
|
wrapped coroutine.
|
|
|
|
|
2017-12-12 18:17:01 -05:00
|
|
|
We modify ``Loop.call_{at,later,soon}`` and
|
|
|
|
``Future.add_done_callback()`` to accept the new optional *context*
|
|
|
|
keyword-only argument, which defaults to the current context::
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
def call_soon(self, callback, *args, context=None):
|
|
|
|
if context is None:
|
2017-12-28 01:01:05 -05:00
|
|
|
context = contextvars.copy_context()
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
# ... some time later
|
|
|
|
context.run(callback, *args)
|
|
|
|
|
2017-12-12 21:39:08 -05:00
|
|
|
Tasks in asyncio need to maintain their own context that they inherit
|
|
|
|
from the point they were created at. ``asyncio.Task`` is modified
|
|
|
|
as follows::
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
class Task:
|
|
|
|
def __init__(self, coro):
|
|
|
|
...
|
|
|
|
# Get the current context snapshot.
|
2017-12-28 01:01:05 -05:00
|
|
|
self._context = contextvars.copy_context()
|
2017-12-12 12:11:31 -05:00
|
|
|
self._loop.call_soon(self._step, context=self._context)
|
|
|
|
|
|
|
|
def _step(self, exc=None):
|
|
|
|
...
|
|
|
|
# Every advance of the wrapped coroutine is done in
|
|
|
|
# the task's context.
|
|
|
|
self._loop.call_soon(self._step, context=self._context)
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
|
Implementation
|
|
|
|
==============
|
|
|
|
|
|
|
|
This section explains high-level implementation details in
|
|
|
|
pseudo-code. Some optimizations are omitted to keep this section
|
|
|
|
short and clear.
|
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
The ``Context`` mapping is implemented using an immutable dictionary.
|
|
|
|
This allows for a O(1) implementation of the ``copy_context()``
|
|
|
|
function. The reference implementation implements the immutable
|
|
|
|
dictionary using Hash Array Mapped Tries (HAMT); see :pep:`550`
|
|
|
|
for analysis of HAMT performance [1]_.
|
|
|
|
|
2017-12-12 23:03:05 -05:00
|
|
|
For the purposes of this section, we implement an immutable dictionary
|
2018-01-17 11:13:59 -05:00
|
|
|
using a copy-on-write approach and the built-in dict type::
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
class _ContextData:
|
|
|
|
|
|
|
|
def __init__(self):
|
2017-12-12 23:03:05 -05:00
|
|
|
self._mapping = dict()
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
def __getitem__(self, key):
|
2017-12-12 23:03:05 -05:00
|
|
|
return self._mapping[key]
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
def __contains__(self, key):
|
|
|
|
return key in self._mapping
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
return len(self._mapping)
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
return iter(self._mapping)
|
|
|
|
|
2017-12-12 12:11:31 -05:00
|
|
|
def set(self, key, value):
|
|
|
|
copy = _ContextData()
|
2017-12-12 23:03:05 -05:00
|
|
|
copy._mapping = self._mapping.copy()
|
|
|
|
copy._mapping[key] = value
|
2017-12-12 12:11:31 -05:00
|
|
|
return copy
|
|
|
|
|
|
|
|
def delete(self, key):
|
|
|
|
copy = _ContextData()
|
2017-12-12 23:03:05 -05:00
|
|
|
copy._mapping = self._mapping.copy()
|
|
|
|
del copy._mapping[key]
|
2017-12-12 12:11:31 -05:00
|
|
|
return copy
|
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
Every OS thread has a reference to the current ``Context`` object::
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2017-12-12 22:40:33 -05:00
|
|
|
class PyThreadState:
|
2018-01-17 08:54:57 -05:00
|
|
|
context: Context
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
``contextvars.Context`` is a wrapper around ``_ContextData``::
|
|
|
|
|
|
|
|
class Context(collections.abc.Mapping):
|
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
_data: _ContextData
|
|
|
|
_prev_context: Optional[Context]
|
|
|
|
|
2017-12-12 12:11:31 -05:00
|
|
|
def __init__(self):
|
2017-12-12 23:03:05 -05:00
|
|
|
self._data = _ContextData()
|
2018-01-17 08:54:57 -05:00
|
|
|
self._prev_context = None
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2017-12-12 23:03:05 -05:00
|
|
|
def run(self, callable, *args, **kwargs):
|
2018-01-17 08:54:57 -05:00
|
|
|
if self._prev_context is not None:
|
|
|
|
raise RuntimeError(
|
|
|
|
f'cannot enter context: {self} is already entered')
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
ts: PyThreadState = PyThreadState_Get()
|
|
|
|
self._prev_context = ts.context
|
2017-12-12 12:11:31 -05:00
|
|
|
try:
|
2018-01-17 08:54:57 -05:00
|
|
|
ts.context = self
|
2017-12-12 23:03:05 -05:00
|
|
|
return callable(*args, **kwargs)
|
2017-12-12 12:11:31 -05:00
|
|
|
finally:
|
2018-01-17 08:54:57 -05:00
|
|
|
ts.context = self._prev_context
|
|
|
|
self._prev_context = None
|
|
|
|
|
|
|
|
def copy(self):
|
|
|
|
new = Context()
|
|
|
|
new._data = self._data
|
|
|
|
return new
|
|
|
|
|
|
|
|
# Implement abstract Mapping.__getitem__
|
|
|
|
def __getitem__(self, var):
|
|
|
|
return self._data[var]
|
|
|
|
|
|
|
|
# Implement abstract Mapping.__contains__
|
|
|
|
def __contains__(self, var):
|
|
|
|
return var in self._data
|
|
|
|
|
|
|
|
# Implement abstract Mapping.__len__
|
|
|
|
def __len__(self):
|
|
|
|
return len(self._data)
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
# Implement abstract Mapping.__iter__
|
|
|
|
def __iter__(self):
|
|
|
|
return iter(self._data)
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
# The rest of the Mapping methods are implemented
|
|
|
|
# by collections.abc.Mapping.
|
|
|
|
|
|
|
|
|
|
|
|
``contextvars.copy_context()`` is implemented as follows::
|
|
|
|
|
|
|
|
def copy_context():
|
|
|
|
ts: PyThreadState = PyThreadState_Get()
|
|
|
|
return ts.context.copy()
|
|
|
|
|
|
|
|
``contextvars.ContextVar`` interacts with ``PyThreadState.context``
|
|
|
|
directly::
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
class ContextVar:
|
|
|
|
|
2017-12-12 23:03:05 -05:00
|
|
|
def __init__(self, name, *, default=_NO_DEFAULT):
|
|
|
|
self._name = name
|
|
|
|
self._default = default
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
2017-12-12 23:03:05 -05:00
|
|
|
return self._name
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2017-12-12 23:03:05 -05:00
|
|
|
def get(self, default=_NO_DEFAULT):
|
2018-01-17 08:54:57 -05:00
|
|
|
ts: PyThreadState = PyThreadState_Get()
|
2017-12-12 12:11:31 -05:00
|
|
|
try:
|
2018-01-17 08:54:57 -05:00
|
|
|
return ts.context[self]
|
2017-12-12 12:11:31 -05:00
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
2017-12-12 23:03:05 -05:00
|
|
|
if default is not _NO_DEFAULT:
|
2017-12-12 12:11:31 -05:00
|
|
|
return default
|
|
|
|
|
2017-12-12 23:03:05 -05:00
|
|
|
if self._default is not _NO_DEFAULT:
|
|
|
|
return self._default
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
raise LookupError
|
|
|
|
|
|
|
|
def set(self, value):
|
2018-01-17 08:54:57 -05:00
|
|
|
ts: PyThreadState = PyThreadState_Get()
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
data: _ContextData = ts.context._data
|
2017-12-12 12:11:31 -05:00
|
|
|
try:
|
2018-01-17 08:54:57 -05:00
|
|
|
old_value = data[self]
|
2017-12-12 12:11:31 -05:00
|
|
|
except KeyError:
|
2017-12-13 12:25:17 -05:00
|
|
|
old_value = Token.MISSING
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
updated_data = data.set(self, value)
|
|
|
|
ts.context._data = updated_data
|
|
|
|
return Token(ts.context, self, old_value)
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
def reset(self, token):
|
2018-01-17 12:42:29 -05:00
|
|
|
if token._used:
|
|
|
|
raise RuntimeError("Token has already been used once")
|
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
if token._var is not self:
|
|
|
|
raise ValueError(
|
|
|
|
"Token was created by a different ContextVar")
|
|
|
|
|
|
|
|
ts: PyThreadState = PyThreadState_Get()
|
|
|
|
if token._context is not ts.context:
|
|
|
|
raise ValueError(
|
|
|
|
"Token was created in a different Context")
|
|
|
|
|
2017-12-13 12:25:17 -05:00
|
|
|
if token._old_value is Token.MISSING:
|
2019-12-23 14:19:34 -05:00
|
|
|
ts.context._data = ts.context._data.delete(token._var)
|
2017-12-12 12:11:31 -05:00
|
|
|
else:
|
2019-12-23 14:19:34 -05:00
|
|
|
ts.context._data = ts.context._data.set(token._var,
|
|
|
|
token._old_value)
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2017-12-12 23:03:05 -05:00
|
|
|
token._used = True
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
Note that the in the reference implementation, ``ContextVar.get()``
|
|
|
|
has an internal cache for the most recent value, which allows to
|
|
|
|
bypass a hash lookup. This is similar to the optimization the
|
|
|
|
``decimal`` module implements to retrieve its context from
|
|
|
|
``PyThreadState_GetDict()``. See :pep:`550` which explains the
|
|
|
|
implementation of the cache in great detail.
|
|
|
|
|
|
|
|
The ``Token`` class is implemented as follows::
|
2017-12-12 12:11:31 -05:00
|
|
|
|
|
|
|
class Token:
|
|
|
|
|
2017-12-13 12:25:17 -05:00
|
|
|
MISSING = object()
|
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
def __init__(self, context, var, old_value):
|
|
|
|
self._context = context
|
2017-12-12 23:03:05 -05:00
|
|
|
self._var = var
|
|
|
|
self._old_value = old_value
|
|
|
|
self._used = False
|
|
|
|
|
2017-12-12 23:42:13 -05:00
|
|
|
@property
|
|
|
|
def var(self):
|
|
|
|
return self._var
|
|
|
|
|
2017-12-13 12:25:17 -05:00
|
|
|
@property
|
|
|
|
def old_value(self):
|
|
|
|
return self._old_value
|
2017-12-12 23:03:05 -05:00
|
|
|
|
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
Summary of the New APIs
|
|
|
|
=======================
|
2017-12-12 23:03:05 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
Python API
|
|
|
|
----------
|
2017-12-12 23:03:05 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
1. A new ``contextvars`` module with ``ContextVar``, ``Context``,
|
|
|
|
and ``Token`` classes, and a ``copy_context()`` function.
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
2. ``asyncio.Loop.call_at()``, ``asyncio.Loop.call_later()``,
|
|
|
|
``asyncio.Loop.call_soon()``, and
|
|
|
|
``asyncio.Future.add_done_callback()`` run callback functions in
|
|
|
|
the context they were called in. A new *context* keyword-only
|
|
|
|
parameter can be used to specify a custom context.
|
2017-12-12 12:11:31 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
3. ``asyncio.Task`` is modified internally to maintain its own
|
|
|
|
context.
|
|
|
|
|
|
|
|
|
|
|
|
C API
|
|
|
|
-----
|
|
|
|
|
2018-09-21 15:37:11 -04:00
|
|
|
1. ``PyObject * PyContextVar_New(char *name, PyObject *default)``:
|
2018-01-17 08:54:57 -05:00
|
|
|
create a ``ContextVar`` object. The *default* argument can be
|
|
|
|
``NULL``, which means that the variable has no default value.
|
|
|
|
|
2018-09-21 15:37:11 -04:00
|
|
|
2. ``int PyContextVar_Get(PyObject *, PyObject *default_value, PyObject **value)``:
|
2018-01-17 08:54:57 -05:00
|
|
|
return ``-1`` if an error occurs during the lookup, ``0`` otherwise.
|
|
|
|
If a value for the context variable is found, it will be set to the
|
|
|
|
``value`` pointer. Otherwise, ``value`` will be set to
|
|
|
|
``default_value`` when it is not ``NULL``. If ``default_value`` is
|
|
|
|
``NULL``, ``value`` will be set to the default value of the
|
|
|
|
variable, which can be ``NULL`` too. ``value`` is always a new
|
|
|
|
reference.
|
|
|
|
|
2018-09-21 15:37:11 -04:00
|
|
|
3. ``PyObject * PyContextVar_Set(PyObject *, PyObject *)``:
|
2018-01-17 08:54:57 -05:00
|
|
|
set the value of the variable in the current context.
|
|
|
|
|
2018-09-21 15:37:11 -04:00
|
|
|
4. ``PyContextVar_Reset(PyObject *, PyObject *)``:
|
2018-01-17 08:54:57 -05:00
|
|
|
reset the value of the context variable.
|
|
|
|
|
2018-09-21 15:37:11 -04:00
|
|
|
5. ``PyObject * PyContext_New()``: create a new empty context.
|
2018-01-17 08:54:57 -05:00
|
|
|
|
2018-09-21 15:37:11 -04:00
|
|
|
6. ``PyObject * PyContext_Copy(PyObject *)``: return a shallow
|
2018-01-21 15:39:33 -05:00
|
|
|
copy of the passed context object.
|
2018-01-17 08:54:57 -05:00
|
|
|
|
2018-09-21 15:37:11 -04:00
|
|
|
7. ``PyObject * PyContext_CopyCurrent()``: get a copy of the current
|
2018-01-21 15:39:33 -05:00
|
|
|
context.
|
|
|
|
|
2018-09-21 15:37:11 -04:00
|
|
|
8. ``int PyContext_Enter(PyObject *)`` and
|
|
|
|
``int PyContext_Exit(PyObject *)`` allow to set and restore
|
2018-01-17 08:54:57 -05:00
|
|
|
the context for the current OS thread. It is required to always
|
|
|
|
restore the previous context::
|
|
|
|
|
2018-09-21 15:37:11 -04:00
|
|
|
PyObject *old_ctx = PyContext_Copy();
|
2018-01-17 08:54:57 -05:00
|
|
|
if (old_ctx == NULL) goto error;
|
2017-12-12 20:41:25 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
if (PyContext_Enter(new_ctx)) goto error;
|
2017-12-12 20:41:25 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
// run some code
|
2017-12-12 20:41:25 -05:00
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
if (PyContext_Exit(old_ctx)) goto error;
|
2017-12-12 20:41:25 -05:00
|
|
|
|
|
|
|
|
2018-01-18 13:49:51 -05:00
|
|
|
Rejected Ideas
|
|
|
|
==============
|
|
|
|
|
2018-01-18 14:33:04 -05:00
|
|
|
Replicating threading.local() interface
|
|
|
|
---------------------------------------
|
2017-12-13 17:06:41 -05:00
|
|
|
|
2018-01-18 13:49:51 -05:00
|
|
|
Please refer to :pep:`550` where this topic is covered in detail: [2]_.
|
|
|
|
|
|
|
|
|
|
|
|
Replacing Token with ContextVar.unset()
|
|
|
|
---------------------------------------
|
2017-12-13 17:06:41 -05:00
|
|
|
|
|
|
|
The Token API allows to get around having a ``ContextVar.unset()``
|
|
|
|
method, which is incompatible with chained contexts design of
|
|
|
|
:pep:`550`. Future compatibility with :pep:`550` is desired
|
2018-01-18 13:49:51 -05:00
|
|
|
in case there is demand to support context variables in generators
|
|
|
|
and asynchronous generators.
|
2017-12-13 17:06:41 -05:00
|
|
|
|
|
|
|
The Token API also offers better usability: the user does not have
|
|
|
|
to special-case absence of a value. Compare::
|
|
|
|
|
2018-01-18 13:49:51 -05:00
|
|
|
token = cv.set(new_value)
|
2017-12-13 17:06:41 -05:00
|
|
|
try:
|
2018-01-18 13:49:51 -05:00
|
|
|
# cv.get() is new_value
|
2017-12-13 17:06:41 -05:00
|
|
|
finally:
|
|
|
|
cv.reset(token)
|
|
|
|
|
|
|
|
with::
|
|
|
|
|
|
|
|
_deleted = object()
|
|
|
|
old = cv.get(default=_deleted)
|
|
|
|
try:
|
|
|
|
cv.set(blah)
|
|
|
|
# code
|
|
|
|
finally:
|
|
|
|
if old is _deleted:
|
|
|
|
cv.unset()
|
|
|
|
else:
|
|
|
|
cv.set(old)
|
|
|
|
|
|
|
|
|
2018-01-18 13:49:51 -05:00
|
|
|
Having Token.reset() instead of ContextVar.reset()
|
|
|
|
--------------------------------------------------
|
2018-01-17 20:27:11 -05:00
|
|
|
|
|
|
|
Nathaniel Smith suggested to implement the ``ContextVar.reset()``
|
|
|
|
method directly on the ``Token`` class, so instead of::
|
|
|
|
|
|
|
|
token = var.set(value)
|
|
|
|
# ...
|
|
|
|
var.reset(token)
|
|
|
|
|
|
|
|
we would write::
|
|
|
|
|
|
|
|
token = var.set(value)
|
|
|
|
# ...
|
|
|
|
token.reset()
|
|
|
|
|
|
|
|
Having ``Token.reset()`` would make it impossible for a user to
|
|
|
|
attempt to reset a variable with a token object created by another
|
|
|
|
variable.
|
|
|
|
|
|
|
|
This proposal was rejected for the reason of ``ContextVar.reset()``
|
|
|
|
being clearer to the human reader of the code which variable is
|
|
|
|
being reset.
|
|
|
|
|
|
|
|
|
2018-01-18 13:49:51 -05:00
|
|
|
Making Context objects picklable
|
|
|
|
--------------------------------
|
2018-01-17 20:27:11 -05:00
|
|
|
|
|
|
|
Proposed by Antoine Pitrou, this could enable transparent
|
|
|
|
cross-process use of ``Context`` objects, so the
|
|
|
|
`Offloading execution to other threads`_ example would work with
|
|
|
|
a ``ProcessPoolExecutor`` too.
|
|
|
|
|
|
|
|
Enabling this is problematic because of the following reasons:
|
|
|
|
|
|
|
|
1. ``ContextVar`` objects do not have ``__module__`` and
|
|
|
|
``__qualname__`` attributes, making straightforward pickling
|
|
|
|
of ``Context`` objects impossible. This is solvable by modifying
|
|
|
|
the API to either auto detect the module where a context variable
|
|
|
|
is defined, or by adding a new keyword-only "module" parameter
|
|
|
|
to ``ContextVar`` constructor.
|
|
|
|
|
|
|
|
2. Not all context variables refer to picklable objects. Making a
|
|
|
|
``ContextVar`` picklable must be an opt-in.
|
|
|
|
|
|
|
|
Given the time frame of the Python 3.7 release schedule it was decided
|
|
|
|
to defer this proposal to Python 3.8.
|
|
|
|
|
|
|
|
|
2018-01-18 13:49:51 -05:00
|
|
|
Making Context a MutableMapping
|
|
|
|
-------------------------------
|
2018-01-18 00:07:14 -05:00
|
|
|
|
|
|
|
Making the ``Context`` class implement the ``abc.MutableMapping``
|
|
|
|
interface would mean that it is possible to set and unset variables
|
|
|
|
using ``Context[var] = value`` and ``del Context[var]`` operations.
|
|
|
|
|
|
|
|
This proposal was deferred to Python 3.8+ because of the following:
|
|
|
|
|
|
|
|
1. If in Python 3.8 it is decided that generators should support
|
|
|
|
context variables (see :pep:`550` and :pep:`568`), then ``Context``
|
|
|
|
would be transformed into a chain-map of context variables mappings
|
|
|
|
(as every generator would have its own mapping). That would make
|
|
|
|
mutation operations like ``Context.__delitem__`` confusing, as
|
|
|
|
they would operate only on the topmost mapping of the chain.
|
|
|
|
|
|
|
|
2. Having a single way of mutating the context
|
|
|
|
(``ContextVar.set()`` and ``ContextVar.reset()`` methods) makes
|
|
|
|
the API more straightforward.
|
|
|
|
|
|
|
|
For example, it would be non-obvious why the below code fragment
|
|
|
|
does not work as expected::
|
|
|
|
|
|
|
|
var = ContextVar('var')
|
|
|
|
|
|
|
|
ctx = copy_context()
|
|
|
|
ctx[var] = 'value'
|
|
|
|
print(ctx[var]) # Prints 'value'
|
|
|
|
|
|
|
|
print(var.get()) # Raises a LookupError
|
|
|
|
|
|
|
|
While the following code would work::
|
|
|
|
|
|
|
|
ctx = copy_context()
|
|
|
|
|
|
|
|
def func():
|
|
|
|
ctx[var] = 'value'
|
|
|
|
|
|
|
|
# Contrary to the previous example, this would work
|
|
|
|
# because 'func()' is running within 'ctx'.
|
|
|
|
print(ctx[var])
|
|
|
|
print(var.get())
|
|
|
|
|
|
|
|
ctx.run(func)
|
|
|
|
|
2018-04-18 14:20:36 -04:00
|
|
|
3. If ``Context`` was mutable it would mean that context variables
|
|
|
|
could be mutated separately (or concurrently) from the code that
|
|
|
|
runs within the context. That would be similar to obtaining a
|
|
|
|
reference to a running Python frame object and modifying its
|
|
|
|
``f_locals`` from another OS thread. Having one single way to
|
|
|
|
assign values to context variables makes contexts conceptually
|
|
|
|
simpler and more predictable, while keeping the door open for
|
|
|
|
future performance optimizations.
|
|
|
|
|
2018-01-18 00:07:14 -05:00
|
|
|
|
2018-01-18 13:49:51 -05:00
|
|
|
Having initial values for ContextVars
|
|
|
|
-------------------------------------
|
2018-01-18 00:07:14 -05:00
|
|
|
|
|
|
|
Nathaniel Smith proposed to have a required ``initial_value``
|
|
|
|
keyword-only argument for the ``ContextVar`` constructor.
|
|
|
|
|
|
|
|
The main argument against this proposal is that for some types
|
|
|
|
there is simply no sensible "initial value" except ``None``.
|
|
|
|
E.g. consider a web framework that stores the current HTTP
|
|
|
|
request object in a context variable. With the current semantics
|
|
|
|
it is possible to create a context variable without a default value::
|
|
|
|
|
|
|
|
# Framework:
|
|
|
|
current_request: ContextVar[Request] = \
|
|
|
|
ContextVar('current_request')
|
|
|
|
|
|
|
|
|
|
|
|
# Later, while handling an HTTP request:
|
|
|
|
request: Request = current_request.get()
|
|
|
|
|
|
|
|
# Work with the 'request' object:
|
|
|
|
return request.method
|
|
|
|
|
|
|
|
Note that in the above example there is no need to check if
|
|
|
|
``request`` is ``None``. It is simply expected that the framework
|
|
|
|
always sets the ``current_request`` variable, or it is a bug (in
|
|
|
|
which case ``current_request.get()`` would raise a ``LookupError``).
|
|
|
|
|
|
|
|
If, however, we had a required initial value, we would have
|
|
|
|
to guard against ``None`` values explicitly::
|
|
|
|
|
|
|
|
# Framework:
|
|
|
|
current_request: ContextVar[Optional[Request]] = \
|
|
|
|
ContextVar('current_request', initial_value=None)
|
|
|
|
|
|
|
|
|
|
|
|
# Later, while handling an HTTP request:
|
|
|
|
request: Optional[Request] = current_request.get()
|
|
|
|
|
|
|
|
# Check if the current request object was set:
|
|
|
|
if request is None:
|
|
|
|
raise RuntimeError
|
|
|
|
|
|
|
|
# Work with the 'request' object:
|
|
|
|
return request.method
|
|
|
|
|
|
|
|
Moreover, we can loosely compare context variables to regular
|
|
|
|
Python variables and to ``threading.local()`` objects. Both
|
|
|
|
of them raise errors on failed lookups (``NameError`` and
|
|
|
|
``AttributeError`` respectively).
|
|
|
|
|
|
|
|
|
2017-12-12 12:11:31 -05:00
|
|
|
Backwards Compatibility
|
|
|
|
=======================
|
|
|
|
|
|
|
|
This proposal preserves 100% backwards compatibility.
|
|
|
|
|
|
|
|
Libraries that use ``threading.local()`` to store context-related
|
|
|
|
values, currently work correctly only for synchronous code. Switching
|
|
|
|
them to use the proposed API will keep their behavior for synchronous
|
|
|
|
code unmodified, but will automatically enable support for
|
|
|
|
asynchronous code.
|
|
|
|
|
|
|
|
|
2018-01-17 08:54:57 -05:00
|
|
|
Examples
|
|
|
|
========
|
|
|
|
|
|
|
|
Converting code that uses threading.local()
|
|
|
|
-------------------------------------------
|
|
|
|
|
|
|
|
A typical code fragment that uses ``threading.local()`` usually
|
|
|
|
looks like the following::
|
|
|
|
|
|
|
|
class PrecisionStorage(threading.local):
|
|
|
|
# Subclass threading.local to specify a default value.
|
|
|
|
value = 0.0
|
|
|
|
|
|
|
|
precision = PrecisionStorage()
|
|
|
|
|
|
|
|
# To set a new precision:
|
|
|
|
precision.value = 0.5
|
|
|
|
|
|
|
|
# To read the current precision:
|
|
|
|
print(precision.value)
|
|
|
|
|
|
|
|
|
|
|
|
Such code can be converted to use the ``contextvars`` module::
|
|
|
|
|
|
|
|
precision = contextvars.ContextVar('precision', default=0.0)
|
|
|
|
|
|
|
|
# To set a new precision:
|
|
|
|
precision.set(0.5)
|
|
|
|
|
|
|
|
# To read the current precision:
|
2018-01-17 11:13:59 -05:00
|
|
|
print(precision.get())
|
2018-01-17 08:54:57 -05:00
|
|
|
|
|
|
|
|
|
|
|
Offloading execution to other threads
|
|
|
|
-------------------------------------
|
|
|
|
|
|
|
|
It is possible to run code in a separate OS thread using a copy
|
|
|
|
of the current thread context::
|
|
|
|
|
|
|
|
executor = ThreadPoolExecutor()
|
|
|
|
current_context = contextvars.copy_context()
|
|
|
|
|
|
|
|
executor.submit(current_context.run, some_function)
|
|
|
|
|
|
|
|
|
2017-12-28 01:01:05 -05:00
|
|
|
Reference Implementation
|
|
|
|
========================
|
|
|
|
|
|
|
|
The reference implementation can be found here: [3]_.
|
2018-01-22 20:11:15 -05:00
|
|
|
See also issue 32436 [4]_.
|
|
|
|
|
|
|
|
|
|
|
|
Acceptance
|
|
|
|
==========
|
|
|
|
|
2022-01-21 06:03:51 -05:00
|
|
|
:pep:`567` was accepted by Guido on Monday, January 22, 2018 [5]_.
|
2018-01-22 20:11:15 -05:00
|
|
|
The reference implementation was merged on the same day.
|
2017-12-28 01:01:05 -05:00
|
|
|
|
|
|
|
|
2017-12-13 17:06:41 -05:00
|
|
|
References
|
|
|
|
==========
|
|
|
|
|
2022-01-21 06:03:51 -05:00
|
|
|
.. [1] :pep:`550#appendix-hamt-performance-analysis`
|
2017-12-13 17:06:41 -05:00
|
|
|
|
2022-01-21 06:03:51 -05:00
|
|
|
.. [2] :pep:`550#replication-of-threading-local-interface`
|
2017-12-13 17:06:41 -05:00
|
|
|
|
2017-12-28 01:01:05 -05:00
|
|
|
.. [3] https://github.com/python/cpython/pull/5027
|
|
|
|
|
2018-01-22 20:11:15 -05:00
|
|
|
.. [4] https://bugs.python.org/issue32436
|
|
|
|
|
|
|
|
.. [5] https://mail.python.org/pipermail/python-dev/2018-January/151878.html
|
|
|
|
|
2018-09-21 15:37:11 -04:00
|
|
|
.. [6] https://bugs.python.org/issue34762
|
|
|
|
|
2017-12-13 17:06:41 -05:00
|
|
|
|
2018-01-19 15:55:02 -05:00
|
|
|
Acknowledgments
|
|
|
|
===============
|
|
|
|
|
|
|
|
I thank Guido van Rossum, Nathaniel Smith, Victor Stinner,
|
2023-10-11 08:05:51 -04:00
|
|
|
Elvis Pranskevichus, Alyssa Coghlan, Antoine Pitrou, INADA Naoki,
|
2018-01-19 15:55:02 -05:00
|
|
|
Paul Moore, Eric Snow, Greg Ewing, and many others for their feedback,
|
|
|
|
ideas, edits, criticism, code reviews, and discussions around
|
|
|
|
this PEP.
|
|
|
|
|
|
|
|
|
2017-12-12 12:11:31 -05:00
|
|
|
Copyright
|
|
|
|
=========
|
|
|
|
|
|
|
|
This document has been placed in the public domain.
|
|
|
|
|
|
|
|
|
|
|
|
..
|
|
|
|
Local Variables:
|
|
|
|
mode: indented-text
|
|
|
|
indent-tabs-mode: nil
|
|
|
|
sentence-end-double-space: t
|
|
|
|
fill-column: 70
|
|
|
|
coding: utf-8
|
|
|
|
End:
|