It seems that the following idea has not been raised here before.  If it
has been, I'd be grateful for a pointer to the relevant discussion.

The idea is: how about adding context manager interface (__enter__ and
__exit__ methods) to the contextvars.Token type?

This would allow code like this:

    with var.set(1):
        # var.get() == 1

that would be equivalent to the following more verbose snippet (taken
from PEP 567):

    token = var.set(1)
    try:
        # var.get() == 1
    finally:
        var.reset(token)

I attach a very rough proof-of-concept implementation of the idea.  The
proper way to implement this proposal would be of course to modify the
_contextvars C-extension.

Here is my motivation for this proposal:

The contextvars module is promoted as a replacement for thread-local
storage for asynchronous programming, but in fact it seems to me that
ContextVars implement thread-safe and async-safe dynamic scoping [1] in
Python.

One of the uses of dynamic scoping is as an alternative to function
parameters for library configuration [2].  While dynamic scoping by
default (as in Emacs Lisp) can be dangerous, I believe that explicit
dynamic scoping could be useful in the Python world as a means to avoid
the "parameter hell" of some libraries, perhaps most infamously
demonstrated by matplotlib.  With the updated ContextVars, a
hypothetical plotting library could be used like this:

with plotting.line_thickness.set(2), plotting.line_style('dashed'):
    plotting.plot(x, y)

As explained in [2], the advantages of this approach compared to
argument passing is that other functions that internally use
plotting.plot do not have to expose all of its configuration options by
themselves as parameters.  Also, with the same mechanism it is possible
to set a parameter for a single invocation of plotting.plot, or a
default value for a whole script.

[1]
https://en.wikipedia.org/wiki/Scope_(computer_science)#Dynamic_scoping
[2] https://www.gnu.org/software/emacs/emacs-paper.html#SEC18

from contextvars import ContextVar


# Define a work-alike to ContextVar that piggybacks on it.

_NO_DEFAULT = '_NO_DEFAULT'

class CMContextVar:
    def __init__(self, name, *, default=_NO_DEFAULT):
        if default is _NO_DEFAULT:
            self._cv = ContextVar(name)
        else:
            self._cv = ContextVar(name, default=default)

    def set(self, value):
        return CMToken(self._cv.set(value))

    def get(self):
        return self._cv.get()

    def reset(self, token):
        return self._cv.reset(token._token)


class CMToken:
    def __init__(self, token):
        self._token = token

# The new bits: context manager interface to CMToken.

    def __enter__(self):
        pass

    def __exit__(self, extype, exval, tb):
        self._token.var.reset(self._token)


def test():
    var = CMContextVar('var', default=0)
    print(var.get())

    # Demonstrate old interface.
    token = var.set(1)
    print(var.get())
    var.reset(token)
    print(var.get())

    # Demonstrate new proposed interface.
    with var.set(2):
        print(var.get())
    print(var.get())


if __name__ == '__main__':
    test()
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to