blob: 42760f4194ea2748382063fc0bc775e425cfa28f [file] [log] [blame]
'''
Introduction
============
Based on the standard Python ``ConfigParser`` module, this module provides an
enhanced configuration parser capabilities. ``Configuration`` is a drop-in
replacement for ``ConfigParser``.
A configuration file is broken into sections, and each section is
introduced by a section name in brackets. For example::
[main]
installationDirectory=/usr/local/foo
programDirectory: /usr/local/foo/programs
[search]
searchCommand: find /usr/local/foo -type f -name "*.class"
[display]
searchFailedMessage=Search failed, sorry.
Section Name Syntax
===================
A section name can consist of alphabetics, numerics, underscores and
periods. There can be any amount of whitespace before and after the
brackets in a section name; the whitespace is ignored.
Variable Syntax
===============
Each section contains zero or more variable settings.
- Similar to a Java ``Properties`` file, the variables are specified as
name/value pairs, separated by an equal sign ("=") or a colon (":").
- Variable names are case-sensitive and may contain alphabetics, numerics,
underscores and periods (".").
- Variable values may contain anything at all. Leading whitespace in the
value is skipped. The way to include leading whitespace in a value is
escape the whitespace characters with backslashes.
Variable Substitution
=====================
A variable value can interpolate the values of other variables, using a
variable substitution syntax. The general form of a variable reference is
``${section_name:var_name}``.
- *section_name* is the name of the section containing the variable to
substitute; if omitted, it defaults to the current section.
- *var_name* is the name of the variable to substitute.
Default values
--------------
You can also specify a default value for a variable, using this syntax::
${foo?default}
${section:foo?default}
That is, the sequence ``?default`` after a variable name specifies the
default value if the variable has no value. (Normally, if a variable has
no value, it is replaced with an empty string.) Defaults can be useful,
for instance, to allow overrides from the environment. The following example
defines a log file directory that defaults to "/tmp", unless environment
variable LOGDIR is set to a non-empty value::
logDirectory: ${env:LOGDIR?/var/log}
Special section names
---------------------
The section names "env", and "program" are reserved for special
pseudosections.
The ``env`` pseudosection
~~~~~~~~~~~~~~~~~~~~~~~~~
The "env" pseudosection is used to interpolate values from the environment. On
UNIX systems, for instance, ``${env:HOME}`` substitutes home directory of the
current user. On some versions of Windows, ``${env:USERNAME}`` will substitute
the name of the user.
Note: On UNIX systems, environment variable names are typically
case-sensitive; for instance, ``${env:USER}`` and ``${env:user}`` refer to
different environment variables. On Windows systems, environment variable
names are typically case-insensitive; ``${env:USERNAME}`` and
``${env:username}`` are equivalent.
The ``program`` pseudosection
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The "program" pseudosection is a placeholder for various special variables
provided by the Configuration class. Those variables are:
``cwd``
The current working directory. Thus, ``${program:cwd}`` will substitute
the working directory, using the appropriate system-specific
file separator (e.g., "/" on Unix, "\\" on Windows).
``name``
The calling program name. Equivalent to the Python expression
``os.path.basename(sys.argv[0])``
``now``
The current time, formatted using the ``time.strftime()`` format
``"%Y-%m-%d %H:%M:%S"`` (e.g., "2008-03-03 16:15:27")
Includes
--------
A special include directive permits inline inclusion of another
configuration file. The include directive takes two forms::
%include "path"
%include "URL"
For example::
%include "/home/bmc/mytools/common.cfg"
%include "http://configs.example.com/mytools/common.cfg"
The included file may contain any content that is valid for this parser. It
may contain just variable definitions (i.e., the contents of a section,
without the section header), or it may contain a complete configuration
file, with individual sections.
Note: Attempting to include a file from itself, either directly or
indirectly, will cause the parser to throw an exception.
Replacing ``ConfigParser``
==========================
You can use this class anywhere you would use the standard Python
``ConfigParser`` class. Thus, to change a piece of code to use enhanced
configuration, you might change this:
.. python::
import ConfigParser
config = ConfigParser.SafeConfigParser()
config.read(configPath)
to this:
.. python::
from grizzled.config import Configuration
config = Configuration()
config.read(configPath)
Sometimes, however, you have to use an API that expects a path to a
configuration file that can *only* be parsed with the (unenhanced)
``ConfigParser`` class. In that case, you simply use the ``preprocess()``
method:
.. python::
import logging
from grizzled import config
logging.config.fileConfig(config.preprocess(pathToConfig))
That will preprocess the enhanced configuration file, producing a file
that is suitable for parsing by the standard Python ``config`` module.
'''
from __future__ import absolute_import
__docformat__ = "restructuredtext en"
# ---------------------------------------------------------------------------
# Imports
# ---------------------------------------------------------------------------
import ConfigParser
import logging
import string
import os
import time
import sys
import re
from grizzled.exception import ExceptionWithMessage
from grizzled.collections import OrderedDict
# ---------------------------------------------------------------------------
# Exports
# ---------------------------------------------------------------------------
__all__ = ['Configuration', 'preprocess',
'NoOptionError', 'NoSectionError', 'NoVariableError']
# ---------------------------------------------------------------------------
# Globals
# ---------------------------------------------------------------------------
log = logging.getLogger('grizzled.config')
NoOptionError = ConfigParser.NoOptionError
NoSectionError = ConfigParser.NoSectionError
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
# Used with _ConfigDict
SECTION_OPTION_DELIM = r':'
# Section name pattern
SECTION_NAME_PATTERN = r'([_.a-zA-Z][_.a-zA-Z0-9]+)'
# Pattern of an identifier local to a section.
VARIABLE_NAME_PATTERN = r'([_a-zA-Z][_a-zA-Z0-9]+)(\?[^}]+)?'
# Pattern of an identifier matched by our version of string.Template.
# Intended to match:
#
# ${section:option} variable 'option' in section 'section'
# ${section:option?default} variable 'option' in section 'section', default
# value 'default'
# ${option} variable 'option' in the current section
# ${option?default} variable 'option' in the current section,
# default value 'default'
VARIABLE_REF_PATTERN = SECTION_NAME_PATTERN + SECTION_OPTION_DELIM +\
VARIABLE_NAME_PATTERN +\
r'|' +\
VARIABLE_NAME_PATTERN
# Simple variable reference
SIMPLE_VARIABLE_REF_PATTERN = r'\$\{' + VARIABLE_NAME_PATTERN + '\}'
# Special sections
ENV_SECTION = 'env'
PROGRAM_SECTION = 'program'
# ---------------------------------------------------------------------------
# Classes
# ---------------------------------------------------------------------------
class NoVariableError(ExceptionWithMessage):
"""
Thrown when a configuration file attempts to substitute a nonexistent
variable, and the ``Configuration`` object was instantiated with
``strict_substitution`` set to ``True``.
"""
pass
class Configuration(ConfigParser.SafeConfigParser):
"""
Configuration file parser. See the module documentation for details.
"""
def __init__(self,
defaults=None,
permit_includes=True,
use_ordered_sections=False,
strict_substitution=False):
"""
Construct a new ``Configuration`` object.
:Parameters:
defaults : dict
dictionary of default values
permit_includes : bool
whether or not to permit includes
use_ordered_sections : bool
whether or not to use an ordered dictionary for the section
names. If ``True``, then a call to ``sections()`` will return
the sections in the order they were encountered in the file.
If ``False``, the order is based on the hash keys for the
sections' names.
strict_substitution : bool
If ``True``, then throw an exception if attempting to
substitute a non-existent variable. Otherwise, simple
substitute an empty value.
"""
ConfigParser.SafeConfigParser.__init__(self, defaults)
self.__permit_includes = permit_includes
self.__use_ordered_sections = use_ordered_sections
self.__strict_substitution = strict_substitution
if use_ordered_sections:
self._sections = OrderedDict()
def defaults(self):
"""
Returns the instance-wide defaults.
:rtype: dict
:return: the instance-wide defaults, or ``None`` if there aren't any
"""
return ConfigParser.SafeConfigParser.defaults(self)
@property
def sections(self):
"""
Get the list of available sections, not including ``DEFAULT``. It's
not really useful to call this method before calling ``read()`` or
``readfp()``.
Returns a list of sections.
"""
return ConfigParser.SafeConfigParser.sections(self)
def add_section(self, section):
"""
Add a section named *section* to the instance. If a section by the
given name already exists, ``DuplicateSectionError`` is raised.
:Parameters:
section : str
name of section to add
:raise DuplicateSectionError: section already exists
"""
ConfigParser.SafeConfigParser.add_section(self, section)
def has_section(self, section):
"""
Determine whether a section exists in the configuration. Ignores
the ``DEFAULT`` section.
:Parameters:
section : str
name of section
:rtype: bool
:return: ``True`` if the section exists in the configuration, ``False``
if not.
"""
return ConfigParser.SafeConfigParser.has_section(self, section)
def options(self, section):
"""
Get a list of options available in the specified section.
:Parameters:
section : str
name of section
:rtype: list
:return: list of available options. May be empty.
:raise NoSectionError: no such section
"""
return ConfigParser.SafeConfigParser.options(self, section)
def has_option(self, section, option):
"""
Determine whether a section has a specific option.
:Parameters:
section : str
name of section
option : str
name of option to check
:rtype: bool
:return: ``True`` if the section exists in the configuration and
has the specified option, ``False`` if not.
"""
return ConfigParser.SafeConfigParser.has_option(self, section, option)
def read(self, filenames):
"""
Attempt to read and parse a list of filenames or URLs, returning a
list of filenames or URLs which were successfully parsed. If
*filenames* is a string or Unicode string, it is treated as a single
filename or URL. If a file or URL named in filenames cannot be opened,
that file will be ignored. This is designed so that you can specify a
list of potential configuration file locations (for example, the
current directory, the user's home directory, and some system-wide
directory), and all existing configuration files in the list will be
read. If none of the named files exist, the ``Configuration`` instance
will contain an empty dataset. An application which requires initial
values to be loaded from a file should load the required file or files
using ``readfp()`` before calling ``read()`` for any optional files:
.. python::
import Configuration
import os
config = Configuration.Configuration()
config.readfp(open('defaults.cfg'))
config.read(['site.cfg', os.path.expanduser('~/.myapp.cfg')])
:Parameters:
filenames : list or string
list of file names or URLs, or the string for a single filename
or URL
:rtype: list
:return: list of successfully parsed filenames or URLs
"""
if isinstance(filenames, basestring):
filenames = [filenames]
newFilenames = []
for filename in filenames:
try:
self.__preprocess(filename, filename)
newFilenames += [filename]
except IOError:
log.exception('Error reading "%s"' % filename)
return newFilenames
def readfp(self, fp, filename=None):
'''
Read and parse configuration data from a file or file-like object.
(Only the ``readline()`` moethod is used.)
:Parameters:
fp : file
File-like object with a ``readline()`` method
filename : str
Name associated with ``fp``, for error messages. If omitted or
``None``, then ``fp.name`` is used. If ``fp`` has no ``name``
attribute, then ``"<???">`` is used.
'''
self.__preprocess(fp, filename)
def get(self, section, option, optional=False):
"""
Get an option from a section.
:Parameters:
section : str
name of section
option : str
name of option to check
optional : bool
``True`` to return None if the option doesn't exist. ``False``
to throw an exception if the option doesn't exist.
:rtype: str
:return: the option value
:raise NoSectionError: no such section
:raise NoOptionError: no such option in the section
"""
def do_get(section, option):
val = ConfigParser.SafeConfigParser.get(self, section, option)
if len(val.strip()) == 0:
raise ConfigParser.NoOptionError(option, section)
return val
if optional:
return self.__get_optional(do_get, section, option)
else:
return do_get(section, option)
def getint(self, section, option, optional=False):
"""
Convenience method that coerces the result of a call to
``get()`` to an ``int``.
:Parameters:
section : str
name of section
option : str
name of option to check
optional : bool
``True`` to return None if the option doesn't exist. ``False``
to throw an exception if the option doesn't exist.
:rtype: int
:return: the option value
:raise NoSectionError: no such section
:raise NoOptionError: no such option in the section
"""
def do_get(section, option):
return ConfigParser.SafeConfigParser.getint(self, section, option)
if optional:
return self.__get_optional(do_xget, section, option)
else:
return do_get(section, option)
def getfloat(self, section, option, optional=False):
"""
Convenience method that coerces the result of a call to ``get()`` to a
``float``.
:Parameters:
section : str
name of section
option : str
name of option to check
optional : bool
``True`` to return None if the option doesn't exist. ``False``
to throw an exception if the option doesn't exist.
:rtype: float
:return: the option value
:raise NoSectionError: no such section
:raise NoOptionError: no such option in the section
"""
def do_get(section, option):
return ConfigParser.SafeConfigParser.getfloat(self, section, option)
if optional:
return self.__get_optional(do_get, section, option)
else:
return do_get(section, option)
def getboolean(self, section, option, optional=False):
'''
Convenience method that coerces the result of a call to ``get()`` to a
boolean. Accepted boolean values are "1", "yes", "true", and "on",
which cause this method to return True, and "0", "no", "false", and
"off", which cause it to return False. These string values are checked
in a case-insensitive manner. Any other value will cause it to raise
``ValueError``.
:Parameters:a
section : str
name of section
option : str
name of option to check
optional : bool
``True`` to return None if the option doesn't exist. ``False``
to throw an exception if the option doesn't exist.
:rtype: bool
:return: the option value (``True`` or ``False``)
:raise NoSectionError: no such section
:raise NoOptionError: no such option in the section
:raise ValueError: non-boolean value encountered
'''
def do_get(section, option):
return ConfigParser.SafeConfigParser.getboolean(self,
section,
option)
if optional:
return self.__get_optional(do_get, section, option)
else:
return do_get(section, option)
def getlist(self, section, option, sep=None, optional=False):
'''
Convenience method that coerces the result of a call to ``get()`` to a
list. The value is split using the separator(s) specified by the
``sep`` argument. A ``sep`` value of ``None`` uses white space. The
result is a list of string values.
:Parameters:
section : str
name of section
option : str
name of option to check
sep : str
list element separator to use. Defaults to white space.
optional : bool
``True`` to return None if the option doesn't exist. ``False``
to throw an exception if the option doesn't exist.
:rtype: bool
:return: the option value (``True`` or ``False``)
:raise NoSectionError: no such section
:raise NoOptionError: no such option in the section
'''
def do_get(section, option):
value = ConfigParser.SafeConfigParser.get(self, section, option)
return value.split(sep)
if optional:
return self.__get_optional(do_get, section, option)
else:
return do_get(section, option)
def get_one_of(self,
section,
options,
optional=False,
default=None,
value_type=str):
'''
Retrieve at most one of a list or set of options from a section. This
method is useful if there are multiple possible names for a single
option. For example, suppose you permit either a ``user_name`` or a
``login_name`` option, but not both, in a section called
``credentials``. You can use the following code to retrieve the
option value:
.. python::
from grizzled.config import Configuration
config = Configuration()
config.read('/path/to/config')
user = config.get_one_of('credentials', ['user_name', 'login_name'])
If both options exist, ``get_one_of()`` will a ``NoOptionError``. If
neither option exists, ``get_one_of()`` will throw a ``NoOptionError``
if ``optional`` is ``False`` and there's no default value; otherwise,
it will return the default value.
:Parameters:
section : str
name of section
options : list or set
list or set of allowable option names
optional : bool
``True`` to return None if the option doesn't exist. ``False``
to throw an exception if the option doesn't exist.
default : str
The default value, if the option does not exist.
value_type : type
The type to which to coerce the value. The value is coerced by
casting.
:rtype: str
:return: the option value, or ``None`` if nonexistent and
``optional`` is ``True``
:raise NoSectionError: no such section
:raise NoOptionError: none of the named options are in the section
'''
value = None
if value_type is bool:
get = self.getboolean
else:
get = self.get
for option in options:
value = get(section, option, optional=True)
if value:
break
if value is None:
value = default
if (value is None) and (not optional):
raise NoOptionError('Section "%s" must contain exactly one of the '
'following options: %s' %
(section, ', '.join(list(options))))
if value is not None:
if not (value_type in (bool, str)):
value = eval('%s(%s)' % (value_type.__name__, value))
return value
def items(self, section):
"""
Get all items in a section.
:Parameters:
section : str
name of section
:rtype: list
:return: a list of (*name*, *value*) tuples for each option in
in *section*
:raise NoSectionError: no such section
"""
return ConfigParser.SafeConfigParser.items(self, section)
def set(self, section, option, value):
"""
If the given section exists, set the given option to the specified
value; otherwise raise ``NoSectionError``.
:Parameters:
section : str
name of section
option : str
name of option to check
value : str
the value to set
:raise NoSectionError: no such section
"""
ConfigParser.SafeConfigParser.set(self, section, option, value)
def write(self, fileobj):
"""
Write a representation of the configuration to the specified file-like
object. This output can be parsed by a future ``read()`` call.
NOTE: Includes and variable references are ``not`` reconstructed.
That is, the configuration data is written in *expanded* form.
:Parameters:
fileobj : file
file-like object to which to write the configuration
"""
ConfigParser.SafeConfigParser.write(self, fileobj)
def remove_section(self, section):
"""
Remove a section from the instance. If a section by the given name
does not exist, ``NoSectionError`` is raised.
:Parameters:
section : str
name of section to remove
:raise NoSectionError: no such section
"""
ConfigParser.SafeConfigParser.remove_section(self, section)
def optionxform(self, option_name):
"""
Transforms the option name in ``option_name`` as found in an input
file or as passed in by client code to the form that should be used in
the internal structures. The default implementation returns a
lower-case version of ``option_name``; subclasses may override this or
client code can set an attribute of this name on instances to affect
this behavior. Setting this to ``str()``, for example, would make
option names case sensitive.
"""
return option_name.lower()
def __get_optional(self, func, section, option):
try:
return func(section, option)
except ConfigParser.NoOptionError:
return None
except ConfigParser.NoSectionError:
return None
def __preprocess(self, fp, name):
try:
fp.name
except AttributeError:
try:
fp.name = name
except TypeError:
# Read-only. Oh, well.
pass
except AttributeError:
# Read-only. Oh, well.
pass
if self.__permit_includes:
# Preprocess includes.
from grizzled.file import includer
tempFile = includer.preprocess(fp)
fp = tempFile
# Parse the resulting file into a local ConfigParser instance.
parsedConfig = ConfigParser.SafeConfigParser()
if self.__use_ordered_sections:
parsedConfig._sections = OrderedDict()
parsedConfig.optionxform = str
parsedConfig.read(fp)
# Process the variable substitutions.
self.__normalizeVariableReferences(parsedConfig)
self.__substituteVariables(parsedConfig)
def __normalizeVariableReferences(self, sourceConfig):
"""
Convert all section-local variable references (i.e., those that don't
specify a section) to fully-qualified references. Necessary for
recursive references to work.
"""
simpleVarRefRe = re.compile(SIMPLE_VARIABLE_REF_PATTERN)
for section in sourceConfig.sections():
for option in sourceConfig.options(section):
value = sourceConfig.get(section, option, raw=True)
oldValue = value
match = simpleVarRefRe.search(value)
while match:
value = value[0:match.start(1)] +\
section +\
SECTION_OPTION_DELIM +\
value[match.start(1):]
match = simpleVarRefRe.search(value)
sourceConfig.set(section, option, value)
def __substituteVariables(self, sourceConfig):
mapping = _ConfigDict(sourceConfig, self.__strict_substitution)
for section in sourceConfig.sections():
mapping.section = section
self.add_section(section)
for option in sourceConfig.options(section):
value = sourceConfig.get(section, option, raw=True)
# Repeatedly substitute, to permit recursive references
previousValue = ''
while value != previousValue:
previousValue = value
value = _ConfigTemplate(value).safe_substitute(mapping)
self.set(section, option, value)
class _ConfigTemplate(string.Template):
"""
Subclass of string.Template that handles our configuration variable
reference syntax.
"""
idpattern = VARIABLE_REF_PATTERN
class _ConfigDict(dict):
"""
Dictionary that knows how to dereference variables within a parsed config.
Only used internally.
"""
idPattern = re.compile(VARIABLE_REF_PATTERN)
def __init__(self, parsedConfig, strict_substitution):
self.__config = parsedConfig
self.__strict_substitution = strict_substitution
self.section = None
def __getitem__(self, key):
try:
# Match against the ID regular expression. (If the match fails,
# it's a bug, since we shouldn't be in here unless it does.)
match = self.idPattern.search(key)
assert(match)
# Now, get the value.
default = None
if SECTION_OPTION_DELIM in key:
if match.group(3):
default = self.__extract_default(match.group(3))
section = match.group(1)
option = match.group(2)
else:
section = self.section
default = None
option = match.group(3)
if match.group(4):
default = self.__extract_default(match.group(3))
result = self.__value_from_section(section, option)
except KeyError:
result = default
except ConfigParser.NoSectionError:
result = default
except ConfigParser.NoOptionError:
result = default
if not result:
if self.__strict_substitution:
raise NoVariableError, 'No such variable: "%s"' % key
else:
result = ''
return result
def __extract_default(self, s):
default = s
if default:
default = default[1:] # strip leading '?'
if len(default) == 0:
default = None
return default
def __value_from_program_section(self, option):
return {
'cwd' : os.getcwd(),
'now' : time.strftime('%Y-%m-%d %H:%M:%S'),
'name' : os.path.basename(sys.argv[0])
}[option]
def __value_from_section(self, section, option):
result = None
if section == 'env':
result = os.environ[option]
if len(result) == 0:
raise KeyError, option
elif section == 'program':
result = self.__value_from_program_section(option)
else:
result = self.__config.get(section, option)
return result
# ---------------------------------------------------------------------------
# Functions
# ---------------------------------------------------------------------------
def preprocess(file_or_url, defaults=None):
"""
This function preprocesses a file or URL for a configuration file,
processing all includes and substituting all variables. It writes a new
configuration file to a temporary file (or specified output file). The
new configuration file can be read by a standard ``ConfigParser``
object. Thus, this method is useful when you have an extended
configuration file that must be passed to a function or object that can
only read a standard ``ConfigParser`` file.
For example, here's how you might use the Python ``logging`` API with an
extended configuration file:
.. python::
from grizzled.config import Configuration
import logging
logging.config.fileConfig(Configuration.preprocess('/path/to/config')
:Parameters:
file_or_url : str
file or URL to read and preprocess
defaults : dict
defaults to pass through to the config parser
:rtype: string
:return: Path to a temporary file containing the expanded configuration.
The file will be deleted when the program exits, though the caller
is free to delete it sooner.
"""
import tempfile
import atexit
def unlink(path):
try:
os.unlink(path)
except:
pass
parser = Configuration(use_ordered_sections=True)
parser.read(file_or_url)
fd, path = tempfile.mkstemp(suffix='.cfg')
atexit.register(unlink, path)
parser.write(os.fdopen(fd, "w"))
return path
# ---------------------------------------------------------------------------
# Main program (for testing)
# ---------------------------------------------------------------------------
if __name__ == '__main__':
import sys
format = '%(asctime)s %(name)s %(levelname)s %(message)s'
logging.basicConfig(level=logging.DEBUG, format=format)
configFile = sys.argv[1]
config = Configuration()
config.read(configFile)
if len(sys.argv) > 2:
for var in sys.argv[2:]:
(section, option) = var.split(':')
val = config.get(section, option, optional=True)
print '%s=%s' % (var, val)
else:
config.write(sys.stdout)