Introduction

Cashew is a plugin system for python, based on the ideas in http://effbot.org/zone/metaclass-plugins.htm.

The goal of Cashew is to let you easily modify the behavior of a tool by using configuration options, and, when you need to, by writing custom code also.

This means that you can configure a system using a data format such as YAML or JSON, or create a nice front-end for users which exposes the available configuration options in a user-friendly way.

You identify a subclass and/or a preconfigured bundle of settings by an alias, and settings can further be modified at runtime to allow for multiple levels of configuration.

Basic Usage

Creating a Base Class

The first step in creating a plugin system is to create a base class, which should be a subclass of the Plugin class, and which should specify PluginMeta as its metaclass:

from cashew import Plugin, PluginMeta
class Data(Plugin):
    """
    Base class for plugins which present data in various ways.
    """
    __metaclass__ = PluginMeta

There’s a little about the __metaclass__ property here in the python docs and several articles online if you search.

You can do anything else you normally do with a base class, like implement instance and class methods to be shared among all subclasses.

    def __init__(self, data):
        self.data = data

    def present(self):
        raise Exception("not implemented")

You can make as many different base classes as you want in your project, and each will be an independent plugin system. For example, in dexy the Filter class, Reporter class, Data class and several others are implemented via plugins.

Plugin Registration

The __init__ method in PluginMeta handles plugin registration:

    def __init__(cls, name, bases, attrs):
        assert issubclass(cls, Plugin), "%s should inherit from class Plugin" % name

        if '__metaclass__' in attrs:
            cls.plugins = {}
        elif hasattr(cls, 'aliases'):
            cls.register_plugin(cls.aliases, cls, {})

        cls.register_other_class_settings()

When this method is called on the base class itself (in our example the Data class), it creates an empty dictionary named plugins, and when it is subsequently called on subclasses, it calls the register_plugin method which populates the plugins dictionary on the base class.

The plugins dictionary keys are the aliases which are defined for each class we want to be accessible, and the values are a tuple of class names (or instances) and dictionaries with settings. The key to Cashew’s flexibility is that the plugins dictionary can be populated directly, as well as being populated automagically when the base class is subclassed.

In order for the automagic to happen, you need to actually load the python modules in which subclasses are defined.

>>> Data.plugins
{}
>>> import classes1
>>> Data.plugins
{'json': (<class 'classes1.Json'>, {'help': ('Helpstring for plugin.', 'JSON type.'), 'aliases': ('aliases', ['json'])}), 'csv': (<class 'classes1.Csv'>, {'help': ('Helpstring for plugin.', 'CSV type.'), 'aliases': ('aliases', ['csv'])})}

A nice way to do this is with a load_plugins module which does nothing but import all the modules in which you have defined plugin subclasses.

To actually use plugins, PluginMeta defines a create_instance factory method which takes an alias as its argument, and optionally accepts positional and keyword arguments to be passed to a constructor.

    def create_instance(cls, alias, *instanceargs, **instancekwargs):
        alias = cls.adjust_alias(alias)

        if not alias in cls.plugins:
            msg = "no alias '%s' available for '%s'"
            msgargs = (alias, cls.__name__)
            raise NoPlugin(msg % msgargs)

        class_or_class_name, settings = cls.plugins[alias]
        klass = cls.get_reference_to_class(class_or_class_name)

        instance = klass(*instanceargs, **instancekwargs)
        instance.alias = alias

        if not hasattr(instance, '_instance_settings'):
            instance.initialize_settings()
        instance.update_settings(settings)

        if not instance.is_active():
            raise InactivePlugin(alias)

        return instance

Here’s an example of using the create_instance method for a plugin based on the Data class. As we saw above, the constructor __init__ takes a single positional argument, and we pass a positional argument to the create_instance method:

>>> json_data = Data.create_instance('json', example_data)
>>> type(json_data)
<class 'classes1.Json'>

Basic Subclassing Example

Here are two examples of simple classes which subclass Data, and each define a different style of presenting the contents of the data attribute:

class Json(Data):
    """
    JSON type.
    """
    aliases = ['json']

    def present(self):
        return json.dumps(self.data)
class Csv(Data):
    """
    CSV type.
    """
    aliases = ['csv']

    def present(self):
        s = StringIO.StringIO()
        writer = csv.DictWriter(s, self.data[0].keys())

        writer.writeheader()
        writer.writerows(self.data)

        return s.getvalue()

Required features:

  • Subclass the Data class (directly or indirectly).

  • Define one or more aliases (unless you don’t want people to be able to use that class directly).

  • Provide a docstring (or a help setting).

Here’s the full usage example, parts of which we have seen already.

We import the Data class from classes.py:

>>> from classes import Data

We define some example_data:

>>> example_data = [{
...     "foo" : 123,
...     "bar" : 456
...     }]

We create an instance of the json plugin, and call its present method:

>>> json_data = Data.create_instance('json', example_data)
>>> json_data.present()
'[{"foo": 123, "bar": 456}]'

And we create an instance of the csv plugin and also call its present method:

>>> csv_data = Data.create_instance('csv', example_data)
>>> csv_data.present()
'foo,bar\r\n123,456\r\n'

Settings

To have user-settable settings, define a dictionary named _settings in your subclass:

    _settings = {
            'write-header' : ("Whether to write a header row first.", True),

            'csv-settings' : (
                "List of settings which should be passed to python csv library.",
                ['dialect', 'delimiter', 'lineterminator']),

            'dialect' : ("CSV dialect.", None),
            'delimiter' : ("A one-character string used to separate fields.", None),
            'lineterminator' : ("String used to terminate lines.", None)
            }

The keys of this dictionary should be hyphen- or underscore-separated setting names, which will be accessible in hyphen format later, and the values should usually be a tuple of (docstring, default value) but may be just a default value if the docstring has already been defined in a parent class.

This dictionary will be combined with any other _settings dictionaries found in any parent class all the way up to the Data base class.

Individual values can be retrieved by calling the setting method and passing the setting name, and all values can be retrieved by calling the setting_values method.

>>> csv_data = Data.create_instance('csv', example_data)
>>>
>>> csv_data.present()
'foo,bar\r\n123,456\r\n'
>>>
>>> csv_data.update_settings({
...     'lineterminator' : '\n',
...     'write_header' : False
...     })
>>>
>>> csv_data.present()
'123,456\n'
>>>
>>> csv_data.setting('lineterminator')
'\n'
>>>
>>> pprint.pprint(csv_data.setting_values())
{'aliases': ['csv'],
 'csv-settings': ['dialect', 'delimiter', 'lineterminator'],
 'delimiter': None,
 'dialect': None,
 'help': 'CSV type.',
 'lineterminator': '\n',
 'write-header': False}

Then in your code, the settings should be used to control any behavior that can be user-customizable. In this case many of the settings are passed directly to the csv library, while the write-header setting is used to determine if the writeheader() method will be called.

    def present(self):
        s = StringIO.StringIO()

        kwargs = dict((k, v)
                for k, v in self.setting_values().iteritems()
                if v and (k in self.setting('csv-settings'))
                )

        writer = csv.DictWriter(s, self.data[0].keys(), **kwargs)

        if self.setting('write-header'):
            writer.writeheader()
        writer.writerows(self.data)

        return s.getvalue()

Settings for Other Classes

You may want to define a plugin which defines a new setting on a different plugin class.

For example in dexy, the website reporter defines some extra parameters on the Document class so that you can specify for each document which website template you’d like to use.

Here is a Report base class which defines an additional setting named bar on a class with alias document:

class Report(Plugin):
    __metaclass__ = PluginMeta
    _settings = {}
    _other_class_settings = {
            'document' : {
                'bar' : ("Bar setting.", None)
                }
            }

And also a Filter base class which defines a foo setting:

class Filter(Plugin):
    __metaclass__ = PluginMeta
    _settings = {}
    _other_class_settings = {
            'document' : {
                'foo' : ("Foo setting", None)
                }
            }

On the Document class, which has alias document, there are no settings defined:

class Document(Plugin):
    __metaclass__ = PluginMeta
    aliases = ['document']

We create a plugin SomeKindOfDocument which is a Document:

class SomeKindOfDocument(Document):
    """
    Some kind.
    """
    aliases = ['somekind']

And, we see that the foo and bar settings are available, even though these were not defined in Document or SomeKindOfDocument:

def test_other_class_settings():
    assert sorted(PluginMeta._store_other_class_settings['document']) == ['bar', 'foo']
    x = Document.create_instance('somekind')
    assert sorted(x.setting_values()) ==  ['aliases', 'bar', 'foo', 'help']

Implementation Details

Plugin Registration

Let’s review the __init__ method and follow the methods it calls.

    def __init__(cls, name, bases, attrs):
        assert issubclass(cls, Plugin), "%s should inherit from class Plugin" % name

        if '__metaclass__' in attrs:
            cls.plugins = {}
        elif hasattr(cls, 'aliases'):
            cls.register_plugin(cls.aliases, cls, {})

        cls.register_other_class_settings()

The first line asserts that our plugin class inherits from Plugin. If this were not the case then lots of expected behavior wouldn’t work.

def test_must_inherit_from_plugin_class():
    try:
        class BadPlugin(object):
            __metaclass__ = PluginMeta

        raise Exception("should not get here")

    except AssertionError as e:
        assert str(e) == "BadPlugin should inherit from class Plugin"

In the next two lines, if we detect __metaclass__ in the class attributes then we are creating a plugin base class, and so we want to initialize a plugins dictionary. If not, then we have already created a base class and we are creating a plugin subclass. In this case, if there are aliases specified, we call the register_plugin method.

    def register_plugin(cls, alias_or_aliases, class_or_class_name, settings):
        aliases = cls.standardize_alias_or_aliases(alias_or_aliases)
        klass = cls.get_reference_to_class(class_or_class_name)

        # Ensure 'aliases' and 'help' settings are set.
        settings['aliases'] = ('aliases', aliases)
        if not settings.has_key('help'):
            docstring = klass.check_docstring()
            settings['help'] = ("Helpstring for plugin.", docstring)

        # Create the tuple which will be registered for the plugin.
        class_info = (class_or_class_name, settings)

        # Register the class_info tuple for each alias.
        for alias in aliases:
            if isinstance(class_or_class_name, type):
                modname = class_or_class_name.__module__
                alias = cls.apply_prefix(modname, alias)

            cls.plugins[alias] = class_info

The register plugin method may receive a list of aliases or a single alias, and it may receive a class name or an actual class. The first thing it does is standardize each of these.

    def standardize_alias_or_aliases(cls, alias_or_aliases):
        """
        Make sure we don't attempt to iterate over an alias string thinking
        it's an array.
        """
        if isinstance(alias_or_aliases, basestring):
            return [alias_or_aliases]
        else:
            return alias_or_aliases
    def get_reference_to_class(cls, class_or_class_name):
        """
        Detect if we get a class or a name, convert a name to a class.
        """
        if isinstance(class_or_class_name, type):
            return class_or_class_name

        elif isinstance(class_or_class_name, basestring):
            if ":" in class_or_class_name:
                mod_name, class_name = class_or_class_name.split(":")

                if not mod_name in sys.modules:
                    __import__(mod_name)

                mod = sys.modules[mod_name]
                return mod.__dict__[class_name]

            else:
                return cls.load_class_from_locals(class_or_class_name)

        else:
            msg = "Unexpected Type '%s'" % type(class_or_class_name)
            raise InternalCashewException(msg)

The get_reference_to_class method will load a fully qualified class name automatically:

def test_get_reference_to_qualified_class():
    pygments_filter_class = Data.get_reference_to_class("dexy.filters.pyg:PygmentsFilter")
    assert pygments_filter_class.__name__ == "PygmentsFilter"

If you want to be able to specify an unqualified class name then you need to establish a mapping between class names and class objects in a load_class_from_locals method, here’s one way to do this:

    @classmethod
    def load_class_from_locals(cls, class_name):
        from example.classes2 import *
        return locals()[class_name]

And this allows you to do:

def test_get_reference_to_class():
    assert Data.get_reference_to_class(Csv) == Csv
    assert Data.get_reference_to_class("Csv") == Csv

You’ll need to implement this one, by default it’s disabled:

    def load_class_from_locals(cls, class_name):
        raise Exception("not implemented")

Here’s the register_plugin source again since it’s been a while since we’ve seen it:

    def register_plugin(cls, alias_or_aliases, class_or_class_name, settings):
        aliases = cls.standardize_alias_or_aliases(alias_or_aliases)
        klass = cls.get_reference_to_class(class_or_class_name)

        # Ensure 'aliases' and 'help' settings are set.
        settings['aliases'] = ('aliases', aliases)
        if not settings.has_key('help'):
            docstring = klass.check_docstring()
            settings['help'] = ("Helpstring for plugin.", docstring)

        # Create the tuple which will be registered for the plugin.
        class_info = (class_or_class_name, settings)

        # Register the class_info tuple for each alias.
        for alias in aliases:
            if isinstance(class_or_class_name, type):
                modname = class_or_class_name.__module__
                alias = cls.apply_prefix(modname, alias)

            cls.plugins[alias] = class_info

The next block of text adds aliases and help settings so we can count on these always being available. You need to provide a docstring which will be used for the help setting.

    def check_docstring(cls):
        """
        Asserts that the class has a docstring, returning it if successful.
        """
        docstring = inspect.getdoc(cls)
        if not docstring:
            breadcrumbs = " -> ".join(t.__name__ for t in inspect.getmro(cls)[:-1][::-1])
            msg = "docstring required for plugin '%s' (%s, defined in %s)"
            args = (cls.__name__, breadcrumbs, cls.__module__)
            raise InternalCashewException(msg % args)
        return docstring

Once the settings are normalized, then we are ready to actually add class information to the plugins dictionary using the aliases as keys.

There’s an option to add namespacing to plugins by implementing a different apply_prefix class method in your plugin base class:

    def apply_prefix(cls, modname, alias):
        return alias

Up to this point we have been looking at registering plugins automatically when their class is loaded, but because a plugin can be registered as an alias linked to a class name and settings dictionary, we can capture this information in a textual format.

The register_plugins method registers multiple plugins based on a dictionary:

    def register_plugins(cls, plugin_info):
        for k, v in plugin_info.iteritems():
            cls.register_plugin(k.split("|"), v[0], v[1])

The dictionaries keys should be aliases, separated by the pipe symbol if there’s more than one of them. The values should be a tuple of class-or-class-name and a settings dictionary. (You can redefine register_plugins or create your own method which calls register_plugin and come up with any other format you want.)

Here’s an example:

def test_register_plugins():
    plugin_info = {
            'foo|altfoo' : ("Data", { "help" : "This is the foo plugin."})
            }

    assert not 'foo' in Data.plugins
    assert not 'altfoo' in Data.plugins

    Data.register_plugins(plugin_info)

    assert 'foo' in Data.plugins
    assert 'altfoo' in Data.plugins

    foo = Data.create_instance('foo', None)
    altfoo = Data.create_instance('altfoo', None)

    assert foo.setting('help') == "This is the foo plugin."
    assert altfoo.setting('help') == "This is the foo plugin."

    assert foo.name() == "Foo"
    assert altfoo.name() == "Foo"

The register_plugins_from_dict method makes it easy to define a simpler data structure (one which will map easily to a YAML file), and it retrieves and removes a class key and generates the required format for calling register_plugin:

    def register_plugins_from_dict(cls, yaml_content):
        for alias, info_dict in yaml_content.iteritems():
            if ":" in alias:
                _, alias = alias.split(":")

            if not info_dict.has_key('class'):
                import json
                msg = "invalid info dict for %s: %s" % (alias, json.dumps(info_dict))
                raise InternalCashewException(msg)

            class_name = info_dict['class']
            del info_dict['class']
            cls.register_plugin(alias.split("|"), class_name, info_dict)
def test_register_plugins_from_dict():
    plugin_info = {
            'bar|altbar' : { "class" : "Data",  "help" : "This is the bar plugin."}
            }

    assert not 'bar' in Data.plugins
    assert not 'altbar' in Data.plugins

    Data.register_plugins_from_dict(plugin_info)

    assert 'bar' in Data.plugins
    assert 'altbar' in Data.plugins

    bar = Data.create_instance('bar', None)
    altbar = Data.create_instance('altbar', None)

    assert bar.setting('help') == "This is the bar plugin."
    assert altbar.setting('help') == "This is the bar plugin."

And here’s a convenience method which registers plugins specified in a YAML file using register_plugins_from_dict:

    def register_plugins_from_yaml_file(cls, yaml_file):
        with open(yaml_file, 'rb') as f:
            yaml_content = yaml.safe_load(f.read())
        cls.register_plugins_from_dict(yaml_content)

Creating Instances

The create_instance method uses the plugins dictionary we just populated to create a new instance of the specified plugin class.

    def create_instance(cls, alias, *instanceargs, **instancekwargs):
        alias = cls.adjust_alias(alias)

        if not alias in cls.plugins:
            msg = "no alias '%s' available for '%s'"
            msgargs = (alias, cls.__name__)
            raise NoPlugin(msg % msgargs)

        class_or_class_name, settings = cls.plugins[alias]
        klass = cls.get_reference_to_class(class_or_class_name)

        instance = klass(*instanceargs, **instancekwargs)
        instance.alias = alias

        if not hasattr(instance, '_instance_settings'):
            instance.initialize_settings()
        instance.update_settings(settings)

        if not instance.is_active():
            raise InactivePlugin(alias)

        return instance

It uses the get_reference_to_class method we’ve already seen to retrive a reference to the class, then creates a new instance. The alias attribute is set on the new instance so we can later retrieve which alias was used to create it.

If any positional or keyword arguments are passed to create_instance (after the alias argument), these are assumed to be constructor arguments and are passed to the constructor.

After the instance is created, we need to initialize the settings to the values specified in various locations.

The initialize_settings method is called. This method is part of the Plugin class, not PluginMeta, so it’s an instance method of our newly created object, not a class method.

    def initialize_settings(self, **raw_kwargs):
        self._instance_settings = {}

        self.initialize_settings_from_parents()
        self.initialize_settings_from_other_classes()
        self.initialize_settings_from_raw_kwargs(raw_kwargs)

The _instance_settings attribute is used to store active settings for a given instance. The subsequent methods populate the dictionary using the update_settings method, which does things like standardize the format from underscore to hyphen, and checks to ensure settings include a help string if this is the first time in the class hierarchy that they have been defined:

    def update_settings(self, new_settings):
        """
        Update settings for this instance based on the provided dictionary of
        setting keys: setting values. Values should be a tuple of (helpstring,
        value,) unless the setting has already been defined in a parent class,
        in which case just pass the desired value.
        """
        self._update_settings(new_settings, False)
    def _update_settings(self, new_settings, enforce_helpstring=True):
        """
        This method does the work of updating settings. Can be passed with
        enforce_helpstring = False which you may want if allowing end users to
        add arbitrary metadata via the settings system.

        Preferable to use update_settings (without leading _) in code to do the
        right thing and always have docstrings.
        """
        for raw_setting_name, value in new_settings.iteritems():
            setting_name = raw_setting_name.replace("_", "-")

            setting_already_exists = self._instance_settings.has_key(setting_name)
            value_is_list_len_2 = isinstance(value, list) and len(value) == 2
            treat_as_tuple = not setting_already_exists and value_is_list_len_2

            if isinstance(value, tuple) or treat_as_tuple:
                self._instance_settings[setting_name] = value

            else:
                if not self._instance_settings.has_key(setting_name):
                    if enforce_helpstring:
                        msg = "You must specify param '%s' as a tuple of (helpstring, value)"
                        raise InternalCashewException(msg % setting_name)

                    else:
                        # Create entry with blank helpstring.
                        self._instance_settings[setting_name] = ('', value,)

                else:
                    # Save inherited helpstring, replace default value.
                    orig = self._instance_settings[setting_name]
                    self._instance_settings[setting_name] = (orig[0], value,)

One issue is that when data is loaded from a file there is no way to distinguish between a tuple and a list of length two, and a list of length two may either be the desired value of a setting, or the first element may be a helpstring and the second element may be the desired value. It is assumed that if the setting does not already exist, then a list of length 2 should be interpreted as a (helpstring, value,) tuple.

Returning to our initialize_settings method:

    def initialize_settings(self, **raw_kwargs):
        self._instance_settings = {}

        self.initialize_settings_from_parents()
        self.initialize_settings_from_other_classes()
        self.initialize_settings_from_raw_kwargs(raw_kwargs)

This is first populated by settings defined in parent classes, starting with the earliest ancestor.

    def initialize_settings_from_parents(self):
        for parent_class in self.__class__.imro():
            if parent_class._settings:
                self.update_settings(parent_class._settings)
            if hasattr(parent_class, '_unset'):
                for unset in parent_class._unset:
                    del self._instance_settings[unset]
    def imro(cls):
        """
        Returns MRO in reverse order, skipping 'object/type' class.
        """
        return reversed(inspect.getmro(cls)[0:-2])

You can provide an _unset list to remove settings you no longer wish to be active:

class UnsetFoo(TestSettingsBase):
    """
    A plugin which unsets the 'foo' setting.
    """
    _unset = ['foo']
    aliases = ['unsetfoo']
def test_unsetting_settings():
    unsetfoo = TestSettingsBase.create_instance('unsetfoo')

    try:
        unsetfoo.setting('foo')
        raise Exception("should not get here")
    except UserFeedback as e:
        assert str(e) == "No setting named 'foo'"

Here’s an example of a setting inheritance, here’s a base class defining a foo setting:

class TestSettingsBase(Plugin):
    """
    Base class for settings class used in tests.
    """
    __metaclass__ = PluginMeta
    _settings = {
            'foo' : ("Foo setting", "This is value of foo set in TestSettingsBase")
            }

Here’s a subclass which doesn’t alter the setting:

class NoSettingsOfMyOwn(TestSettingsBase):
    """
    A plugin which doesn't override foo.
    """
    aliases = ['nosettingsofmyown', 'twoaliases']
def test_no_settings_of_my_own():
    nosettingsofmyown = TestSettingsBase.create_instance('nosettingsofmyown')
    assert nosettingsofmyown.setting('foo') == "This is value of foo set in TestSettingsBase"

Here’s a subclass which does:

class OverrideFooSetting(TestSettingsBase):
    """
    A plugin which sets a different value for foo and also defines a new setting bar.
    """
    aliases = ['overridefoosetting']
    _settings = {
            'foo' : "I am overriding foo.",
            'bar' : ("The bar setting.", "Default value for the bar setting.")
            }
def test_override_settings():
    override = TestSettingsBase.create_instance('overridefoosetting')
    assert override.setting('foo') == "I am overriding foo."
    assert override.setting('bar') == "Default value for the bar setting."

Next, we initialize settings which may have been specified by other classes:

    def initialize_settings_from_other_classes(self):
        if hasattr(self.__class__, 'aliases') and self.__class__.aliases:
            for parent_class in self.__class__.imro():
                for alias in parent_class.aliases:
                    settings_from_other_classes = PluginMeta._store_other_class_settings.get(alias)
                    if settings_from_other_classes:
                        self.update_settings(settings_from_other_classes)

And then we initialize settings using any kwargs that were passed to initialize_settings:

    def initialize_settings_from_raw_kwargs(self, raw_kwargs):
        hyphen_settings = dict(
                (k, v)
                for k, v in raw_kwargs.iteritems()
                if k in self._instance_settings)

        underscore_settings = dict(
                (k.replace("_", "-"), v)
                for k, v in raw_kwargs.iteritems()
                if k.replace("_", "-") in self._instance_settings)

        self.update_settings(hyphen_settings)
        self.update_settings(underscore_settings)

Now returning to create_instance:

    def create_instance(cls, alias, *instanceargs, **instancekwargs):
        alias = cls.adjust_alias(alias)

        if not alias in cls.plugins:
            msg = "no alias '%s' available for '%s'"
            msgargs = (alias, cls.__name__)
            raise NoPlugin(msg % msgargs)

        class_or_class_name, settings = cls.plugins[alias]
        klass = cls.get_reference_to_class(class_or_class_name)

        instance = klass(*instanceargs, **instancekwargs)
        instance.alias = alias

        if not hasattr(instance, '_instance_settings'):
            instance.initialize_settings()
        instance.update_settings(settings)

        if not instance.is_active():
            raise InactivePlugin(alias)

        return instance

We see there is another call to update_settings and this is where settings stored in the plugins dictionary are applied.

This is because initialize_settings may be called in the constructor and if so it does not get called again here, so a separate call to update_settings is required.

In addition to creating individual instances using create_instance, it is possible to iterate over an instance of each type of plugin.

    def __iter__(cls, *instanceargs):
        """
        Lets you iterate over instances of all plugins which are not marked as
        'inactive'. If there are multiple aliases, the resulting plugin is only
        called once.
        """
        processed_aliases = set()
        for alias in sorted(cls.plugins):
            if alias in processed_aliases:
                # duplicate alias
                continue

            try:
                instance = cls.create_instance(alias, *instanceargs)
                yield(instance)
                for alias in instance.aliases:
                    processed_aliases.add(alias)

            except InactivePlugin:
                pass
def test_iter():
    aliases = sorted(instance.alias for instance in TestSettingsBase)
    assert aliases == ['nosettingsofmyown', 'overridefoosetting', 'unsetfoo']

Retrieving Settings

Individual setting values should be obtained by calling the setting method:

    def setting(self, name_hyphen):
        """
        Retrieves the setting value whose name is indicated by name_hyphen.

        Values starting with $ are assumed to reference environment variables,
        and the value stored in environment variables is retrieved. It's an
        error if thes corresponding environment variable it not set.
        """
        if name_hyphen in self._instance_settings:
            value = self._instance_settings[name_hyphen][1]
        else:
            msg = "No setting named '%s'" % name_hyphen
            raise UserFeedback(msg)

        if hasattr(value, 'startswith') and value.startswith("$"):
            env_var = value.lstrip("$")
            if os.environ.has_key(env_var):
                return os.getenv(env_var)
            else:
                msg = "'%s' is not defined in your environment" % env_var
                raise UserFeedback(msg)

        elif hasattr(value, 'startswith') and value.startswith("\$"):
            return value.replace("\$", "$")

        else:
            return value

If you don’t want a UserFeedback exception raised if the setting you ask for doesn’t exist, you can use the safe_setting method instead:

    def safe_setting(self, name_hyphen, default=None):
        """
        Retrieves the setting value, but returns a default value rather than
        raising an error if the setting does not exist.
        """
        try:
            return self.setting(name_hyphen)
        except UserFeedback:
            return default
def test_safe_setting():
    data = Data.create_instance('csv', None)
    assert data.setting('write-header') == True
    assert data.safe_setting('write-header') == True
    assert data.safe_setting('right-header') == None

The setting_values method returns a dictionary of all setting values:

    def setting_values(self, skip=None):
        """
        Returns dict of all setting values (removes the helpstrings).
        """
        if not skip:
            skip = []

        return dict(
                (k, v[1])
                for k, v in self._instance_settings.iteritems()
                if not k in skip)

If a setting starts with a dollar sign, it is assumed to be an environment variable, and the setting method will retreive the value of that environment variable.

def test_retrieve_environment_variables():
    data = Data.create_instance('csv', None)
    data.update_settings({ "working-dir" : ("The current working directory.", "$PWD")})
    assert 'cashew' in data.setting('working-dir')

If there is no corresponding env var defined, a UserFeedback exception is raised:

def test_error_if_no_env_var():
    data = Data.create_instance('csv', None)
    data.update_settings({ "fake-env-var" : ("", "$DOESNOTEXIST")})
    try:
        data.setting('fake-env-var')
    except UserFeedback as e:
        assert str(e) == "'DOESNOTEXIST' is not defined in your environment"

If you actually need to start a value with a dollar sign, you can escape it:

def test_escaped_dollar_sign():
    data = Data.create_instance('csv', None)
    data.update_settings({ "not-a-var" : ("This is not an environment variable.", "\$PWD")})
    assert data.setting('not-a-var') == "$PWD"