# -*- coding: utf-8 -*- """ Cooperative ``contextvars`` module. This module was added to Python 3.7. The gevent version is available on all supported versions of Python. However, see an important note about gevent 20.9. Context variables are like greenlet-local variables, just more inconvenient to use. They were designed to work around limitations in :mod:`asyncio` and are rarely needed by greenlet-based code. The primary difference is that snapshots of the state of all context variables in a given greenlet can be taken, and later restored for execution; modifications to context variables are "scoped" to the duration that a particular context is active. (This state-restoration support is rarely useful for greenlets because instead of always running "tasks" sequentially within a single thread like `asyncio` does, greenlet-based code usually spawns new greenlets to handle each task.) The gevent implementation is based on the Python reference implementation from :pep:`567` and doesn't have much optimization. In particular, setting context values isn't constant time. .. versionadded:: 1.5a3 .. versionchanged:: 20.9.0 On Python 3.7 and above, this module is no longer monkey-patched in place of the standard library version. gevent depends on greenlet 0.4.17 which includes support for context variables. This means that any number of greenlets can be running any number of asyncio tasks each with their own context variables. This module is only greenlet aware, not asyncio task aware, so its use is not recommended on Python 3.7 and above. On previous versions of Python, this module continues to be a solution for backporting code. It is also available if you wish to use the contextvar API in a strictly greenlet-local manner. """ from __future__ import absolute_import from __future__ import division from __future__ import print_function __all__ = [ 'ContextVar', 'Context', 'copy_context', 'Token', ] try: from collections.abc import Mapping except ImportError: from collections import Mapping # pylint:disable=deprecated-class from gevent._util import _NONE from gevent.local import local __stdlib_expected__ = __all__ __implements__ = __stdlib_expected__ # In the reference implementation, the interpreter level OS thread state # is modified to contain a pointer to the current context. Obviously we can't # touch that here because we're not tied to CPython's internals; plus, of course, # we want to operate with greenlets, not OS threads. So we use a greenlet-local object # to store the active context. class _ContextState(local): def __init__(self): self.context = Context() def _not_base_type(cls): # This is not given in the PEP but is tested in test_context. # Assign this method to __init_subclass__ in each type that can't # be subclassed. (This only works in 3.6+, but context vars are only in # 3.7+) raise TypeError("not an acceptable base type") class _ContextData(object): """ A copy-on-write immutable mapping from ContextVar keys to arbitrary values. Setting values requires a copy, making it O(n), not O(1). """ # In theory, the HAMT used by the stdlib contextvars module could # be used: It's often available at _testcapi.hamt() (see # test_context). We'd need to be sure to add a correct __hash__ # method to ContextVar to make that work well. (See # Python/context.c:contextvar_generate_hash.) __slots__ = ( '_mapping', ) def __init__(self): self._mapping = {} def __getitem__(self, key): return self._mapping[key] def __contains__(self, key): return key in self._mapping def __len__(self): return len(self._mapping) def __iter__(self): return iter(self._mapping) def set(self, key, value): copy = _ContextData() copy._mapping = self._mapping.copy() copy._mapping[key] = value return copy def delete(self, key): copy = _ContextData() copy._mapping = self._mapping.copy() del copy._mapping[key] return copy class ContextVar(object): """ Implementation of :class:`contextvars.ContextVar`. """ __slots__ = ( '_name', '_default', ) def __init__(self, name, default=_NONE): self._name = name self._default = default __init_subclass__ = classmethod(_not_base_type) @classmethod def __class_getitem__(cls, _): # For typing support: ContextVar[str]. # Not in the PEP. # sigh. return cls @property def name(self): return self._name def get(self, default=_NONE): context = _context_state.context try: return context[self] except KeyError: pass if default is not _NONE: return default if self._default is not _NONE: return self._default raise LookupError def set(self, value): context = _context_state.context return context._set_value(self, value) def reset(self, token): token._reset(self) def __repr__(self): # This is not captured in the PEP but is tested by test_context return '<%s.%s name=%r default=%r at 0x%x>' % ( type(self).__module__, type(self).__name__, self._name, self._default, id(self) ) class Token(object): """ Opaque implementation of :class:`contextvars.Token`. """ MISSING = _NONE __slots__ = ( '_context', '_var', '_old_value', '_used', ) def __init__(self, context, var, old_value): self._context = context self._var = var self._old_value = old_value self._used = False __init_subclass__ = classmethod(_not_base_type) @property def var(self): """ A read-only attribute pointing to the variable that created the token """ return self._var @property def old_value(self): """ A read-only attribute set to the value the variable had before the ``set()`` call, or to :attr:`MISSING` if the variable wasn't set before. """ return self._old_value def _reset(self, var): if self._used: raise RuntimeError("Taken has already been used once") if self._var is not var: raise ValueError("Token was created by a different ContextVar") if self._context is not _context_state.context: raise ValueError("Token was created in a different Context") self._used = True if self._old_value is self.MISSING: self._context._delete(var) else: self._context._reset_value(var, self._old_value) def __repr__(self): # This is not captured in the PEP but is tested by test_context return '<%s.%s%s var=%r at 0x%x>' % ( type(self).__module__, type(self).__name__, ' used' if self._used else '', self._var, id(self), ) class Context(Mapping): """ Implementation of :class:`contextvars.Context` """ __slots__ = ( '_data', '_prev_context', ) def __init__(self): """ Creates an empty context. """ self._data = _ContextData() self._prev_context = None __init_subclass__ = classmethod(_not_base_type) def run(self, function, *args, **kwargs): if self._prev_context is not None: raise RuntimeError( "Cannot enter context; %s is already entered" % (self,) ) self._prev_context = _context_state.context try: _context_state.context = self return function(*args, **kwargs) finally: _context_state.context = self._prev_context self._prev_context = None def copy(self): """ Return a shallow copy. """ result = Context() result._data = self._data return result ### # Operations used by ContextVar and Token ### def _set_value(self, var, value): try: old_value = self._data[var] except KeyError: old_value = Token.MISSING self._data = self._data.set(var, value) return Token(self, var, old_value) def _delete(self, var): self._data = self._data.delete(var) def _reset_value(self, var, old_value): self._data = self._data.set(var, old_value) # 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: # # - context[var] raises a KeyError, # - var in context returns False, # - the variable isn't included in context.items(), etc. # Checking the type of key isn't part of the PEP but is tested by # test_context.py. @staticmethod def __check_key(key): if type(key) is not ContextVar: # pylint:disable=unidiomatic-typecheck raise TypeError("ContextVar key was expected") def __getitem__(self, key): self.__check_key(key) return self._data[key] def __contains__(self, key): self.__check_key(key) return key in self._data def __len__(self): return len(self._data) def __iter__(self): return iter(self._data) def copy_context(): """ Return a shallow copy of the current context. """ return _context_state.context.copy() _context_state = _ContextState()