An Example

Because the capability of options is designed for high-end, edge-case situations, it’s hard to demonstrate its virtues with simple code. But we’ll give it a shot.

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={0!r}, width={1}, name={2!r}, height={3}".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. It gets repetitive, and is not pretty. Another classic alternative, using native keyword arguments, is no 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={0!r}, width={1}, name={2!r}, height={3}".format(color, width, name, height))

If we add just a few more instance variables, we’ve arrived at the Mr. Creosote of class design. For every instance variable that might be overridden in a method call, that method needs one line of code to decide whether the override is, in fact, in effect. And that line will appear in every method call needing to support such overrides. Suddenly, dealing with parameters starts to be a full-time job and responsibility of every method. That’s neither elegant nor scalable. Pretty soon we’re in “just one more wafer-thin mint…” territory.

But with options, it’s easy. No matter how many configuration variables there are to be managed, each method needs just one line of code to manage them:

opts = self.options.push(kwargs)

Changing things works simply and logically:

Shape.options.set(color='blue')
one.draw()
one.options.set(color='red')
one.draw(height=100)
one.draw(height=44, color='yellow')

yields:

color='blue', width=10, name='one', height=10
color='red', width=10, name='one', height=100
color='yellow', width=10, name='one', height=44

In one line, we reset the default color for all Shape objects. That’s visible in the next call to one.draw(). Then we set the instance color of one (all other Shape instances remain blue). Finally, we call one with a temporary override of the color.

A common pattern makes this even easier:

class Shape(OptionsClass):
    ...

The OptionsClass base class will provide a set() method so that you don’t need Shape.options.set(). Shape.set() does the same thing, resulting in an even simpler API. The set() method is a “combomethod” that will take either a class or an instance and “do the right thing.” OptionsClass also provides a settings() method to easily handle transient with contexts (more on this in a minute), and a __repr__() method so that it prints nicely.

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 situations–but it’s poor pattern for complex option and configuration handling.

For richly-featured APIs, options’s delegation pattern is simpler. As the number of options grows, delegation imposes almost no additional coding, complexity, or failure modes. Options are consolidated in one place, providing neat attribute-style access and keeping everything 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 option stacking. Every option set is stacked on top of previously set option sets, with lower-level values shining through if they’re not set at higher levels. This stacking or overlay model resembles how local and global variables are managed in many programming languages.

This makes advanced use cases, such as temporary value changes, easy:

with one.settings(height=200, color='purple'):
    one.draw()
    if is_tall(one):
        ...         # it is, but only within the ``with`` context

if is_tall(one):    # nope, not here!
    ...

Note

You will still need to do some housekeeping in your class’s __init__() method, including creating a new options layer. If you don’t wish to inherit from OptionsClass, you can manually add set() and settings() methods to your own class. See the OptionsClass source code for details.

As one final feature, consider “magical” parameters. Add the following code to your class description:

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 specified with int (integer/numeric) values, your module auto-magically supports relative parameters for height and width given as string parameters.:

one.draw(width='+200')

yields:

color='blue', width=210, name='one', height=10

Neat, huh?

For more backstory, see this StackOverflow.com discussion of how to combat “configuration sprawl”. For examples of options in use, see say and show.