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"