HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Dictionary as context manager

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
contextdictionarymanager

Problem

I've created a class that implements customized addition, subtraction, etc. But for this class I'd like to have some additional parameters for these methods (__add__, ...).

So my first thought was: A global dictionary that can be set or altered before I do the operations and internally it just accesses this dictionary and assumes these were given as kwargs. This obviously solved my problem but another one emerged: I had to keep track of this global dictionary and it's state.

So I thought I can solve this with a dictionary that can be used as context manager that allows me to set them temporarly but cleans up afterwards:

from copy import deepcopy

class Arguments(object):
    """A dictionary container that can be used as context manager.

    The context manager allows to modify the dictionary values and after
    exiting it resets them to the original state.

    Parameters
    ----------
    kwargs :
        Initial values for the contained dictionary.

    Attributes
    ----------
    defaults : dict
        The `dict` containing the defaults as key-value pairs
    """
    defaults = {}

    def __init__(self, **kwargs):
        # Copy the original and update the current dictionary with the values
        # passed in.
        self.dct_copy = deepcopy(self.defaults)
        self.defaults.update(kwargs)

    def __enter__(self):
        # return the dictionary so one can catch it if one wants don't want to
        # always update the class attribute or change some defaults in between
        return self.defaults

    def __exit__(self, type, value, traceback):
        # clear the dictionary (in case someone added a new value) and update
        # it with the original values again
        self.defaults.clear()
        self.defaults.update(self.dct_copy)


Do you think this approach is good? Are there alternatives or even builtin or plugins that do essentially the same (maybe some configuration-like class)? Is the code fairly straightforward without to

Solution

What you’re trying to do reminds me a lot of decimal's contexts. Basically, you have setcontext and getcontext to manipulate the current context, they are a wrappers around threading and thread-local objects to be able to manage a different context per thread if need be. In your case, if you don't plan on supporting threads, a global object can do (as is your Arguments.defaults).

And then localcontext which is a thin layer around _ContextManager which performs pretty much what Arguments do: save a context using getcontext on __enter__ and restore it using setcontext on __exit__.

For reference, here are the relevant parts of decimal:

def setcontext(context):
    if context in (DefaultContext, BasicContext, ExtendedContext):
        context = context.copy()
        context.clear_flags()
    threading.current_thread().__decimal_context__ = context

def getcontext():
    try:
        return threading.current_thread().__decimal_context__
    except AttributeError:
        context = Context()
        threading.current_thread().__decimal_context__ = context
        return context


I picked the parts not using thread-locals here but the principle is the same if they are enabled.

And for the context manager part:

def localcontext(ctx=None):
    if ctx is None: ctx = getcontext()
    return _ContextManager(ctx)

class _ContextManager(object):
    def __init__(self, new_context):
        self.new_context = new_context.copy()
    def __enter__(self):
        self.saved_context = getcontext()
        setcontext(self.new_context)
        return self.new_context
    def __exit__(self, t, v, tb):
        setcontext(self.saved_context)


Based on that, if you don't plan on using threads, you can simplify the design:

import copy

class Arguments:
    def __init__(self, **kwargs):
        self.update(kwargs)

    def update(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

def get_arguments():
    global _arguments
    try:
        return _arguments
    except NameError:
        _arguments = Arguments()
        return _arguments

def set_arguments(arg=None, **kwargs):
    global _arguments
    if arg is None:
        arg = get_arguments()
    _arguments = copy.copy(arg)
    _arguments.update(kwargs)

class local_arguments:
    def __init__(self, arg=None, **kwargs):
        if arg is None:
            arg = get_arguments()
        self.new_arguments = copy.copy(arg)
        self.new_arguments.update(kwargs)
    def __enter__(self):
        self.old_arguments = get_arguments()
        set_arguments(self.new_arguments)
        return self.new_arguments
    def __exit__(self, t, v, tb):
        set_arguments(self.old_arguments)


The advantage of such approach is that you can directly use get_arguments in the magic methods, without having to rely on a delegated one:

class Container(object):
    def __init__(self, data):
        self.data = data

    def __add__(self, other):
        args = get_arguments()
        print(self, other, args)


But if you want to add extra methods to manualy supply extra arguments, it is simplified thanks to local_argument:

def add(self, other, **kwargs):
        with local_arguments(**kwargs):
            return self + other


Global usage is still pretty much the same, though:

set_arguments(foo=42, bar='baz')

with local_arguments(foo=8):
    Container(2) + 10

Code Snippets

def setcontext(context):
    if context in (DefaultContext, BasicContext, ExtendedContext):
        context = context.copy()
        context.clear_flags()
    threading.current_thread().__decimal_context__ = context


def getcontext():
    try:
        return threading.current_thread().__decimal_context__
    except AttributeError:
        context = Context()
        threading.current_thread().__decimal_context__ = context
        return context
def localcontext(ctx=None):
    if ctx is None: ctx = getcontext()
    return _ContextManager(ctx)


class _ContextManager(object):
    def __init__(self, new_context):
        self.new_context = new_context.copy()
    def __enter__(self):
        self.saved_context = getcontext()
        setcontext(self.new_context)
        return self.new_context
    def __exit__(self, t, v, tb):
        setcontext(self.saved_context)
import copy

class Arguments:
    def __init__(self, **kwargs):
        self.update(kwargs)

    def update(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

def get_arguments():
    global _arguments
    try:
        return _arguments
    except NameError:
        _arguments = Arguments()
        return _arguments

def set_arguments(arg=None, **kwargs):
    global _arguments
    if arg is None:
        arg = get_arguments()
    _arguments = copy.copy(arg)
    _arguments.update(kwargs)

class local_arguments:
    def __init__(self, arg=None, **kwargs):
        if arg is None:
            arg = get_arguments()
        self.new_arguments = copy.copy(arg)
        self.new_arguments.update(kwargs)
    def __enter__(self):
        self.old_arguments = get_arguments()
        set_arguments(self.new_arguments)
        return self.new_arguments
    def __exit__(self, t, v, tb):
        set_arguments(self.old_arguments)
class Container(object):
    def __init__(self, data):
        self.data = data

    def __add__(self, other):
        args = get_arguments()
        print(self, other, args)
def add(self, other, **kwargs):
        with local_arguments(**kwargs):
            return self + other

Context

StackExchange Code Review Q#128985, answer score: 2

Revisions (0)

No revisions yet.