options /

Jonathan Eunice 75a8c49 


from stuf import orderedstuf
from chainstuf import chainstuf
from nulltype import NullType

Unset = NullType()

def attrs(m, first=[], underscores=False):
    Given a mapping m, return a string listing its values in a
    key=value format. Items with underscores are, by default, not
    listed. If you want some things listed first, include them in
    the list first.
    keys = first[:]
    for k in m.keys():
        if not underscores and k.startswith('_'):
        if k not in first:
    return ', '.join([ "{}={}".format(k, repr(m[k])) for k in keys ])

class Options(orderedstuf):
    Options handler.
    def __init__(self, *args, **kwargs):
        orderedstuf.__init__(self, *args, **kwargs)
        self._magic = {}
    def __repr__(self):
        return "{}({})".format(self.__class__.__name__, attrs(self))
    def set(self, **kwargs):
        # To set values at the base options level, create a temporary next level,
        # which will have the magical option interpretation. Then copy the resulting
        # values here. Do in original ordering.
        oc = OptionsChain(self, kwargs)
        for key in self.keys():
            self[key] = oc[key] 
    def push(self, kwargs):        
        return OptionsChain(self, kwargs)
    def magic(self, **kwargs):
        Set some options as having 'magical' update properties. NB no magical
        processing is done to the base Options. These are assumed to have whatever
        adjustments are needed when they are originally set.
        magical = self.get('_magic', {})
        for k, v in kwargs.items():
            if hasattr(v, '__func__'):  # special handling for static methods
                v = v.__func__
            magical[k] = v
        self._magic = magical
    def magical(self, key):
        Instance based decorator, specifying a function in the using module
        as a magical function. Note that the magical methods will be called
        with a self of None. 
        def my_decorator(func):
            self._magic[key] = func
            return func
        return my_decorator
        # not sure why we take such care to get the order right for Options, because the
        # dict handed to Options.__init__ is not in order!
        # should there be a leftovers_ok option that raises an error on push()
        # if there are leftovers?

class OptionsChain(chainstuf):
    def __init__(self, bottom, kwargs):
        Create an OptionsChain, pushing one level down.
        chainstuf.__init__(self, bottom)
        processed = self._process(bottom, kwargs)
        self.maps = [ processed, bottom ]
    def _magicalized(self, key, value):
        Get the magically processed value for a single key value pair.
        If there is no magical processing to be done, just returns value.
        magicfn = self._magic.get(key, None)
        if magicfn is None:
            return value
        argcount = magicfn.func_code.co_argcount
        if argcount == 1:
            return magicfn(value)
        elif argcount == 2:
            return magicfn(value, self)
        elif argcount == 3:
            return magicfn(None, value, self)
            raise ValueError('magic function should have 1-3 arguments, not {}'.format(argcount))

    def _process(self, base, kwargs):
        Given kwargs, removes any key:value pairs corresponding to this set of
        options. Those pairs are interpreted according to'paramater
        interpretation magic' if needed, then returned as dict. Any key:value
        pairs remaining in kwargs are not options related to this class, and may
        be used for other purposes.

        opts = {}
        for key, value in kwargs.items():
            if key in base:
                opts[key] = self._magicalized(key, value)
        # NB base identical to self when called from set(), but not when called
        # from __init__()
        # empty kwargs of 'taken' options
        for key in opts:
            del kwargs[key]

        return opts
    def __repr__(self):
        Get repr() of OptionsChain. Dig down to find earliest ancestor, which
        contains the right ordering of keys.
        grandpa = self.maps[-1]
        n_layers  = len(self.maps)
        while type(grandpa) is not Options:
            grandpa = grandpa.maps[-1]
            n_layers += len(grandpa.maps) - 1
        guts = attrs(self, first=list(grandpa.keys()), underscores=True)
        return "{}({} layers: {})".format(self.__class__.__name__, n_layers, guts)
    def push(self, kwargs):
        return OptionsChain(self, kwargs)
    def set(self, **kwargs):
        newopts = self._process(self, kwargs)
        for k, v in newopts.items():
            if v is Unset:
                del self.maps[0][k]
                self.maps[0][k] = v
    #def __setattr__(self, name, value):
    #    print "OptionsChain.__setattr__() name:", name, "value:", value
    #    if name in self:
    #        print "    in self"
    #        if value is Unset and name in self.maps[0]:
    #            # only unset the very top level
    #            del self.maps[0][name]
    #        else:
    #            self[name] = self._magicalized(name, value)
    #    else:
    #        print "    not in self; punt to superclass"
    #        chainstuf.__setattr__(self, name, value)

    # could possibly extend set() magic to setattr (but would have to be
    # careful of recursions). Current draft doesn't work - infinite recursion

class OptionsContext(object):
    Context manager so that modules that use Options can easily implement
    a `with x.settings(...):` capability. In x's class:
    def settings(self, **kwargs):
        return OptionsContext(self, kwargs)

    def __init__(self, caller, kwargs):
        When `with x.method(*args, **kwargs)` is called, it creates an OptionsContext
        passing in its **kwargs. 
        self.caller = caller        
        if 'opts' in kwargs:
            newopts = OptionsChain(caller.options, kwargs['opts'])
            newopts.maps.insert(0, caller._process(newopts, kwargs))
            newopts = OptionsChain(caller.options, kwargs)
        caller.options = newopts

    def __enter__(self):
        Called when the `with` is about to be 'entered'. Whatever this returns
        will be the value of `x` if the `as x` construction is used. Not generally
        needed for option setting, but might be needed in a subclass.
        return self.caller
    def __exit__(self, exc_type, exc_value, traceback):
        Called when leaving the `with`. Reset caller's options to what they were
        before we entered.
        self.caller.options = self.caller.options.maps[-1]

    # Using a with statement and OptionsContext can effectively reduce the
    # thread safety of an object, even to NIL. This is because the object is
    # modified for an indeterminate period. It would be possible to improve
    # thread safety with an construction, if what was returned by as
    # were a proxy that didn't modify the original object. Another approach
    # might be to provide per-thread options, with _get_options() looking
    # options up in a tid-indexed hash, and set() operations creating a copy
    # (copy-on-write, per thread, essentially).