Commits

Jonathan Eunice  committed 75a8c49

clean repo

  • Participants

Comments (0)

Files changed (8)

+syntax: glob
+*.swp.{py,txt,html,css,js}
+*.pyc
+.DS_Store
+build/*
+dist/*
+*.egg-info
+setup.cfg
+PKG-INFO
+A module that helps encapsulate option and configuration data using a
+multi-layer stacking model.
+
+Classes, for example, set default values for all
+instances. Instances can set new values. If an instance doesn't set a value, the
+class-set default "shines through" and remains in effect. Individual method
+calls can also set transient values that apply just for that call. If the call
+doesn't set a value, the instance value applies. If the instance didn't set a
+value, the class default applies.
+
+This layered or stacked approach is particularly helpful when you have highly
+functional classes that aim for "reasonable" or "intelligent" defaults and
+behaviors, yet that allow users to override those defaults at any time, and that
+aim for a simple, unobtrusive API.
+
+This 
+option-handling pattern is based on delegation rather than inheritance. It's
+described in `this StackOverflow.com discussion of "configuration sprawl" 
+<http://stackoverflow.com/questions/11702437/where-to-keep-options-values-paths-to-important-files-etc/11703813#11703813>`_.
+
+Unfortunately, it's a bit hard to demonstrate the virtues of this approach with
+simple code. Python already has pretty flexible function arguments, inlcuding
+variable number of arguments (``*args``), keyword arguments, and optional
+keyword arguments (``**kwargs``). Combined with object inheritance, base Python
+already covers a large number of use cases. But when you have a large number of
+configuration and instance variables, and when you might want to temporarily
+override either class or instance settings, things get dicey. This messy,
+complicated space is where ``options`` truly begins to shine.
+
+Usage
+=====
+
+::
+
+    from options import Options, attrs
+    
+    class Shape(object):
+    
+        options = Options(
+            name   = None,
+            color  = 'white',
+            height = 10,
+            width  = 10,
+        )
+        
+        def __init__(self, **kwargs):
+            self.options = Shape.options.push(kwargs)
+        
+        def draw(self, **kwargs):
+            opts = self.options.push(kwargs)
+            print attrs(opts)
+
+    one = Shape(name='one')
+    one.draw()
+    one.draw(color='red')
+    one.draw(color='green', width=22)
+    
+yielding::
+
+    color='white', width=10, name='one', height=10
+    color='red', width=10, name='one', height=10
+    color='green', width=22, name='one', height=10
+
+So far we could do this with instance variables and standard arguments. It
+might look a bit like this::
+
+    class ClassicShape(object):
+
+        def __init__(self, name=None, color='white', height=10, width=10):
+            self.name   = name
+            self.color  = color
+            self.height = height
+            self.width  = width
+
+but when we got to the ``draw`` method, things would be quite a bit messier.::
+
+        def draw(self, **kwargs):
+            name   = kwargs.get('name',   self.name)
+            color  = kwargs.get('color',  self.color)
+            height = kwargs.get('height', self.height)
+            width  = kwargs.get('width',  self.width)
+            print "color='{}', width={}, name='{}', height={}".format(color, width, name, height)
+        
+One problem here is that we broke apart the values provided to ``__init__()`` into
+separate instance variables, now we need to re-assemble them into something unified.
+And we need to explicitly choose between the ``**kwargs`` and the instance variables.
+This is pretty repetitive, and not very pretty. Another classic alternative, using
+native keyword arguments, is not much
+better::
+
+        def draw2(self, name=None, color=None, height=None, width=None):
+            name   = name   or self.name
+            color  = color  or self.color
+            height = height or self.height
+            width  = width  or self.width
+            print "color='{}', width={}, name='{}', height={}".format(color, width, name, height)
+
+If we add just a few more instance variables, we have the `Mr. Creosote <http://en.wikipedia.org/wiki/Mr_Creosote>`_
+of class design on our hands. Not good. Things get worse if we want to set
+default values for all shapes in the class. We have to rework every method that
+uses values, the ``__init__`` method, *et cetera*. We've entered
+"just one more wafer-thin
+mint..." territory.
+
+But with ``options``, it's easy::
+
+    Shape.options.set(color='blue')
+    one.draw()
+    one.draw(height=100)
+    one.draw(height=44, color='yellow')
+    
+yields::
+
+    color='blue', width=10, name='one', height=10
+    color='blue', width=10, name='one', height=100
+    color='yellow', width=10, name='one', height=44
+
+In one line, we reset the default for all ``Shape`` objects.
+
+The more options and settings a class has, the more unwieldy the class and
+instance variable approach becomes, and the more desirable the delegation
+alternative. Inheritance is a great software pattern for many kinds of data and
+program structures, but it's a bad pattern for complex option and configuration
+handling. For richly featured classes, the delegation pattern used by
+``options`` is much simpler. Even a large number of options requires almost no
+additional code and imposes no additional complexity or failure modes. By consolidating
+options into one place, and by allowing neat attribute-style access, everything is
+kept tidy. We can add new options or methods with confidence::
+
+    def is_tall(self, **kwargs):
+        opts = self.options.push(kwargs)
+        return opts.height > 100
+
+Under the covers, ``options`` uses a variation on the ``ChainMap``
+data structure
+(a multi-layer dictionary) to provide its option stacking. Every option set is
+stacked on top of previously set option sets, with lower-level values shinging
+through if they're not set at higher levels. This stacking or overlay model
+resmebles how local and global variables are managed in many programming
+languages.
+
+Magic Parameters
+================
+
+Python's ``*args`` variable-number of arguments and ``**kwargs`` keyword
+arguments are sometimes called "magic" arguments. ``options`` takes this up
+a notch, allowing arguments to be interpreted on the fly. This is useful, for instance,
+to provide relative rather than just absolute values. As an example, say that
+we added this code after
+``Shape.options``
+was defined::
+
+    options.magic(
+        height = lambda v, cur: cur.height + int(v) if isinstance(v, str) else v,
+        width  = lambda v, cur: cur.width  + int(v) if isinstance(v, str) else v
+    )
+    
+Now, in addition to absolute ``height`` and ``width`` parameters which are provided
+by specifying those values as ``int``, your module
+auto-magically supports relative parameters.::
+
+    one.draw(width='+200')
+    
+yields::
+
+    color='blue', width=210, name='one', height=10
+    
+This can be as fancy as you like, defining an entire domain-specific expression language.
+But even small functions can give you a great bump in expressive power. For example,
+add this and you get full relative arithmetic capability (``+``, ``-``, ``*``, and ``/``)::
+
+    def relmath(value, currently):
+        if isinstance(value, str):
+            if value.startswith('*'):
+                return currently * int(value[1:])
+            elif value.startswith('/'):
+                return currently / int(value[1:])
+            else:
+                return currently + int(value)
+        else:
+            return value
+    
+    ...
+    
+    options.magic(
+        height = lambda v, cur: relmath(v, cur.height),
+        width  = lambda v, cur: relmath(v, cur.width)
+    )
+
+Then::
+
+    one.draw(width='*4', height='/2')
+
+yields::
+
+    color='blue', width=40, name='one', height=5
+    
+Magically interpreted parameters are the sort of thing that one doesn't need
+very often or for every parameter--but when 
+it's useful, it's *enormously* useful and highly leveraged, leading
+to much simpler, much higher function
+APIs.
+
+Design Considerations
+=====================
+
+In general, classes will define a set of methods that are "outwards facing"--methods 
+called by external code when consuming the class's functionality.
+Those methods should generally expose their options through ``**kwargs``,
+creating a local variable (say ``opts``) that represents the sum of all options
+in use--the full stack of call, instance, and class options, including
+any present magical interpretations.
+
+Internal class methods--the sort that are not generally called by external code,
+and that by Python convention are often prefixed by an underscore (``_``)--these
+generally do not need ``**kwargs``. They should accept their options as a
+single variable (say ``opts`` again) that the externally-facing methods will
+provide.
+
+For example, if ``options`` didn't provide the nice formatting function ``attrs``,
+we might have designed our own::
+
+    def _attrs(self, opts):
+        nicekeys = [ k for k in opts.keys() if not k.startswith('_') ]
+        return ', '.join([ "{}={}".format(k, repr(opts[k])) for k in nicekeys ])
+   
+    def draw(self, **kwargs):
+        opts = self.options.push(kwargs)
+        print self._attrs(opts)
+        
+``draw()``, being the outward-facing API, accepts general arguments and
+manages their stacking (by ``push``ing ``kwargs`` onto the instance options).
+When the internal ``_attrs()`` method is called, it is handed a pre-digested
+``opts`` package of options.
+
+A nice side-effect of making this distinction: Whenever you see a method with
+``**kwargs``, you know it's outward-facing. When you see a method with just
+``opts``, you know it's internal.
+
+Objects defined with ``options`` make excellent "callables."
+Define the ``__call__`` method, and you have a very nice analog of
+function calls.
+
+``options`` has broad utility, but it's not for every class or module. It best
+suits high-level front-end APIs that multiplex lots of potential functionality, and
+wish/need to do it in a clean/simple way. Classes for which the set of instance
+variables is small, or methods for which the set of known/possible parameters is
+limited--these work just fine with classic Python calling conventions. "Horses
+for courses."
+
+Setting and Unsetting
+=====================
+
+Using ``options``, objects often become "entry points" that represent both
+a set of capabilities and a set of configurations for how that functionality
+will be used. As a result, you may want to be able to set the object's
+values directly, without referencing their underlying ``options``. It's
+convenient to add a ``set()`` method, then use it, as follows::
+
+    def set(self, **kwargs):
+        self.options.set(**kwargs)
+        
+    one.set(width='*10', color='orange')
+    one.draw()
+    
+yields::
+
+    color='orange', width=100, name='one', height=10
+
+``one.set()`` is now the equivalent of ``one.options.set()``. Or continue using
+the ``options`` attribute explicitly, if you prefer that.
+
+Values can also be unset.::
+
+    from options import Unset
+
+    one.set(color=Unset)
+    one.draw()
+    
+yields::
+
+    color='blue', width=100, name='one', height=10
+    
+Because ``'blue'`` was the color to which ``Shape`` had be most recently set.
+With the color of the instance unset, the color of the class shines through.
+
+**NOTA BENE** while options are ideally accessed with an attribute notion,
+the preferred of setting options is through method calls: ``set()`` if
+accessing directly, or ``push()`` if stacking values as part of a method call.
+These perform the interpretation and unsetting magic;
+straight assignment does not. In the future, ``options`` may provide an
+equivalent ``__setattr__()`` method to allow assignment--but not yet.
+
+Leftovers
+=========
+
+``options`` expects you to define all feasible and legitimate options at the
+class level, and to give them reasonable defaults.
+
+None of the initial settings ever have magic applied. Much of the
+expected interpretation "magic" will be relative settings, and relative settings
+require a baseline value. The top level is expected and demanded to provide a
+reasonable baseline.
+
+Any options set "further down" such as when an instance is created or a method
+called should set keys that were already-defined at the class level.
+
+However, there are cases where "extra" ``**kwargs`` values may be provided and
+make sense. Your object might be a very high level entry point, for example,
+representing very large buckets of functionality, with many options. Some of
+those options are relevant to the current instance, while others are intended as
+pass-throughs for lower-level modules. This may seem a rarified case--and it is,
+relatively speaking. But it does happen, and when you need that kind of
+multi-level processing, it's really, really super amazingly handy to have.
+
+``options`` supports this in its core ``push()`` method by taking the values
+that are known to be part of the class's options, and deleting those from
+``kwargs``. Any values left over in the ``kwargs`` ``dict`` are either errors,
+or intended for other recipients.
+
+As yet, there is no automatic check for leftovers.
+
+The Magic APIs
+==============
+
+The callables (usually functions, lambda expressions, static methods, or methods) called
+to preform magical interpretation can be called with 1, 2, or 3 parameters.
+``options`` inquires as to how many parameters the callable accepts. If it
+accepts only 1, it will be the value passed in. Cleanups like "convert to upper case"
+can be done, but no relative interpretation. If it accepts 2 arguments,
+it will be called with the value and the current option mapping, in that order.
+(NB this order reverses the way you may think logical. Caution advised.) If the
+callable requires 3 parameters, it will be ``None``, value, current mapping. This
+supports method calls, though has the defect of not really
+passing in the current instance.
+
+A decorator form, ``magical()`` is also supported. It must be given the
+name of the key exactly::
+
+    @options.magical('name')
+    def capitalize_name(self, v, cur):
+        return ' '.join(w.capitalize() for w in v.split())
+
+The net is that you can provide just about any kind of callable.
+But the meta-programming of the magic interpretation API could use a little work.
+
+Notes
+=====
+
+ * This is a work in progress. The underlying have
+   been successfully used in multiple projects, but it remains in an evolving
+   state as a standalone module. The API may change over time.
+   Swim at your own risk.
+ 
+ * The author, `Jonathan Eunice <mailto:jonathan.eunice@gmail.com>`_ or
+   `@jeunice on Twitter <http://twitter.com/jeunice>`_
+   welcomes your comments and suggestions.
+
+Installation
+============
+
+::
+
+    pip install options
+    
+(You may need to prefix this with "sudo " to authorize installation.)
+from options import *
+
+"""
+Demonstration code used in, or while writing, the documentation.
+"""
+
+from options import Options, attrs, Unset
+
+class ClassicShape(object):
+    
+    name   = 'Shapes Rule!'
+    color  = 'purple'
+    height = 50
+    width  = 50
+
+    def __init__(self, name=None, color='white', height=10, width=10):
+        self.name   = name
+        self.color  = color
+        self.height = height
+        self.width  = width
+    
+    def draw(self, **kwargs):
+        name   = kwargs.get('name',   self.name)
+        color  = kwargs.get('color',  self.color)
+        height = kwargs.get('height', self.height)
+        width  = kwargs.get('width',  self.width)
+        print "color='{}', width={}, name='{}', height={}".format(color, width, name, height)
+        
+    def draw2(self, name=None, color=None, height=None, width=None):
+        name   = name   or self.name
+        color  = color  or self.color
+        height = height or self.height
+        width  = width  or self.width
+        print "color='{}', width={}, name='{}', height={}".format(color, width, name, height)
+        
+    def draw3(self, name=None, color=None, height=None, width=None):
+        name   = name   or self.name   or ClassicShape.name
+        color  = color  or self.color  or ClassicShape.color
+        height = height or self.height or ClassicShape.height
+        width  = width  or self.width  or ClassicShape.width
+        print "color='{}', width={}, name='{}', height={}".format(color, width, name, height)
+        
+oldone = ClassicShape(name='one')
+oldone.draw()
+oldone.draw(color='red')
+oldone.draw(color='green', width=22)
+
+print "--"
+oldone.draw2()
+oldone.draw2(color='red')
+oldone.draw2(color='green', width=22)
+
+print "--"
+oldone.draw3()
+oldone.draw3(color='red')
+oldone.draw3(color='green', width=22)
+
+print '==='
+
+
+
+def relative_meta(key):
+    def setter(v, current):
+        return int(v) + current[key] if isinstance(v, str) else v
+    return setter
+
+def relative(value, currently):
+    return int(value) + currently if isinstance(value, str) else value
+
+def relmath(value, currently):
+    if isinstance(value, str):
+        if value.startswith('*'):
+            return currently * int(value[1:])
+        elif value.startswith('/'):
+            return currently / int(value[1:])
+        else:
+            return currently + int(value)
+    else:
+        return value
+    
+class Shape(object):
+
+    options = Options(
+        name   = None,
+        color  = 'white',
+        height = 10,
+        width  = 10,
+    )
+    
+    options.magic(
+        height = lambda v, cur: cur.height + int(v) if isinstance(v, str) else v,
+        width  = lambda v, cur: cur.height + int(v) + cur.width  if isinstance(v, str) else v,
+    )
+    
+    options.magic(
+        height = lambda v, cur: relmath(v, cur.height),
+        width  = lambda v, cur: relmath(v, cur.width)
+    )
+  
+    
+    def __init__(self, **kwargs):
+        self.options = Shape.options.push(kwargs)
+    
+    def _attrs(self, opts):
+        nicekeys = [ k for k in opts.keys() if not k.startswith('_') ]
+        return ', '.join([ "{}={}".format(k, repr(opts[k])) for k in nicekeys ])
+
+    def draw(self, **kwargs):
+        opts = self.options.push(kwargs)
+        print attrs(opts)
+        
+    def draw2(self, **kwargs):
+        opts = self.options.push(kwargs)
+        print self._attrs(opts)
+        
+    def set(self, **kwargs):
+        self.options.set(**kwargs)
+        
+    def is_tall(self, **kwargs):
+        opts = self.options.push(kwargs)
+        return opts.height > 100
+    
+    @options.magical('name')
+    def capitalize_name(self, v, cur):
+        return ' '.join(w.capitalize() for w in v.split())
+
+one = Shape(name='one')
+one.draw()
+one.draw(color='red')
+one.draw(color='green', width=22)
+
+print '--'
+Shape.options.set(color='blue')
+one.draw()
+one.draw(height=100)
+one.draw(height=44, color='yellow')
+
+print '---'
+one.draw(width='+200')
+one.draw()
+
+print '----'
+one.draw(width='*4', height='/2')
+one.draw2(width='*4', height='/2')
+
+print '-----'
+one.set(width='*10', color='orange')
+one.draw()
+one.set(color=Unset)
+one.draw()
+
+print "------"
+one.set(name='a shape')
+one.draw()
+import six
+
+class NullType(object):
+    """
+    A 'null' type different from, but parallel to, None. Core function
+    is representing emptyness in a way that doesn't overload None.
+    
+    Instantiate to create desired Null values.
+    """
+    if six.PY3:
+        def bool(self):
+            """I am always False."""
+            return False
+    else:
+        def __nonzero__(self):
+            """I am always False."""
+            return False
+"""Options"""
+
+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('_'):
+            continue
+        if k not in first:
+            keys.append(k)
+    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)
+        else:
+            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]
+            else:
+                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))
+        else:
+            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 with..as 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).
+#! /usr/bin/env python
+
+from setuptools import setup
+from decimal import Decimal
+import re
+
+def linelist(text):
+    """
+    Returns each non-blank line in text enclosed in a list.
+    """
+    return [ l.strip() for l in text.strip().splitlines() if l.split() ]
+    
+    # The double-mention of l.strip() is yet another fine example of why
+    # Python needs en passant aliasing.
+
+
+def verno(s):
+    """
+    Update the version number passed in by extending it to the 
+    thousands place and adding 1/1000, then returning that result
+    and as a side-effect updating setup.py
+
+    Dangerous, self-modifying, and also, helps keep version numbers
+    ascending without human intervention.
+    """
+    d = Decimal(s)
+    increment = Decimal('0.001')
+    d = d.quantize(increment) + increment
+    dstr = str(d)
+    setup = open('setup.py', 'r').read()
+    setup = re.sub('verno\(\w*[\'"]([\d\.]+)[\'"]', 'verno("' + dstr + '"', setup)
+    open('setup.py', 'w').write(setup)
+    return dstr
+
+setup(
+    name='options',
+    version=verno("0.012"),
+    author='Jonathan Eunice',
+    author_email='jonathan.eunice@gmail.com',
+    description='Container for flexible class, instance, and function call options',
+    long_description=open('README.rst').read(),
+    url='',
+    py_modules=['options', 'nulltype'],
+    install_requires=['stuf', 'otherstuf', 'six'],
+    classifiers=linelist("""
+        Development Status :: 3 - Alpha
+        Operating System :: OS Independent
+        License :: OSI Approved :: BSD License
+        Intended Audience :: Developers
+        Programming Language :: Python
+        Topic :: Software Development :: Libraries :: Python Modules
+    """)
+)

File test/test.py

+
+from testharness import import_from_parent, test_run
+
+import_from_parent()
+
+from options import *
+    
+def test_good_chainstuf():
+    """Test options class for being faithful subclass of chainstuf"""
+    
+    # make some base dictsw
+    d1 = Options(this=1, that=2, roger=None)
+    d2 = dict(roger=99, that=100)
+    
+    # test simple attribute equivalence
+    dd = d1.push(d2)
+    assert dd.this == 1
+    assert dd.roger == 99
+    assert dd.this == dd['this']
+    assert dd.that == dd['that']
+    
+    assert dd.roger == dd['roger']
+    
+    # set value, ensure properly set, in top dict
+    dd.roger = 'wilco'
+    assert dd.roger == 'wilco'
+
+def test_unset():
+    d1 = Options(this=1, that=2, roger=None)
+    d2 = dict(roger=99, that=100)
+    dd = d1.push(d2)
+    d3 = dict(fish='wanda', that='fish')
+    de = dd.push(d3)
+
+    assert de.that == 'fish'
+    de.set(that=Unset)
+    assert de.that == 100
+    de.set(that='fishy')
+    assert de.that == 'fishy'
+    de.set(that=Unset)
+    assert de.that == 100
+
+    assert dd.that == 100
+    dd.set(that=Unset)
+    assert dd.that == 2
+    assert de.that == 2
+    
+def test_magic():
+    
+    o = Options(this=1, slick='slack', blik=99)
+    o['_magic'] = { 'slick': lambda x: x.capitalize() }
+    
+    # test that magic doesnt effect everything
+    o.blik = "wubber"
+    assert o.blik == 'wubber'
+
+    # test that it does effect the right things
+    o.set(slick='mack')
+    assert o.slick == 'Mack'  # note magical capitalization
+    
+    p = o.push(dict(this=2))
+    assert p.this == 2
+    assert p.slick == 'Mack'
+    
+   
+def test_magic_designation():
+    
+    class T(object):
+        options = Options(
+            this=1,
+            slick='slack',
+            nick='nack',
+            blik=99,
+            man='man'
+        )
+        
+        # technique 1: lambda expression or function
+        options.magic(
+            slick = lambda v: v.capitalize(),
+        )
+        
+        def __init__(self, *args, **kwargs):
+            self.options = T.options.push(kwargs)
+            self.data = args
+            
+        def set(self, **kwargs):
+            """
+            Uplevel the set operation. A set() on this object is converted into
+            a set on its options.
+            """
+            self.options.set(**kwargs)
+            
+        # technique 2 - a static method with after-the-fact inclusion
+        
+        @staticmethod
+        def nick_magic(v, cur):
+            return v.upper()
+    
+        options.magic(nick=nick_magic)
+
+        # technique 3 - a decorated method
+        
+        @options.magical('man')
+        def man_magic(self, v, cur):
+            return v.upper()
+
+    
+    t = T()
+    assert t.options.this == 1
+    assert t.options.blik == 99
+    assert t.options.nick == 'nack'
+    assert t.options.slick == 'slack'
+    t.set(slick='slack')
+    assert t.options.slick == 'Slack'
+    t.set(slick='wack')
+    assert t.options.slick =='Wack'
+    t.set(nick='flick')
+    assert t.options.nick == 'FLICK'
+    
+    t.set(man='boy')
+    assert t.options.man == 'BOY'
+    t.set(man='girl')
+    assert t.options.man == 'GIRL'    
+    
+if __name__ == '__main__':
+    test_run()