# -*- 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.

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
"""
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

from gevent._compat import PY37
from gevent._util import _NONE
from gevent.local import local

__stdlib_expected__ = __all__
__implements__ = __stdlib_expected__ if PY37 else None

# 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 = dict()

    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()
