lazr.config-2.1/ 0000775 0001750 0001750 00000000000 12452641133 014053 5 ustar barry barry 0000000 0000000 lazr.config-2.1/MANIFEST.in 0000664 0001750 0001750 00000000150 12452627145 015614 0 ustar barry barry 0000000 0000000 include *.py *.txt *.rst MANIFEST.in *.ini
recursive-include lazr *.txt *.rst *.conf
exclude .bzrignore
lazr.config-2.1/lazr/ 0000775 0001750 0001750 00000000000 12452641133 015023 5 ustar barry barry 0000000 0000000 lazr.config-2.1/lazr/config/ 0000775 0001750 0001750 00000000000 12452641133 016270 5 ustar barry barry 0000000 0000000 lazr.config-2.1/lazr/config/_config.py 0000664 0001750 0001750 00000074236 12452636367 020277 0 ustar barry barry 0000000 0000000 # Copyright 2008-2015 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.config.
#
# lazr.config is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.config is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.config. If not, see .
"""Implementation classes for config."""
from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'Config',
'ConfigData',
'ConfigSchema',
'ImplicitTypeSchema',
'ImplicitTypeSection',
'Section',
'SectionSchema',
'as_boolean',
'as_host_port',
'as_log_level',
'as_timedelta',
'as_username_groupname',
]
import datetime
import grp
import logging
import os
import pwd
import re
import sys
from os.path import abspath, basename, dirname
from textwrap import dedent
try:
from io import StringIO
from configparser import NoSectionError, RawConfigParser
except ImportError:
# Python 2.
from StringIO import StringIO
from ConfigParser import NoSectionError, RawConfigParser
from zope.interface import implementer
from lazr.config.interfaces import (
ConfigErrors, ICategory, IConfigData, IConfigLoader, IConfigSchema,
InvalidSectionNameError, ISection, ISectionSchema, IStackableConfig,
NoCategoryError, NoConfigError, RedefinedSectionError, UnknownKeyError,
UnknownSectionError)
from lazr.delegates import delegate_to
_missing = object()
def read_content(filename):
"""Return the content of a file at filename as a string."""
with open(filename, 'rt') as fp:
return fp.read()
@implementer(ISectionSchema)
class SectionSchema:
"""See `ISectionSchema`."""
def __init__(self, name, options, is_optional=False, is_master=False):
"""Create an `ISectionSchema` from the name and options.
:param name: A string. The name of the ISectionSchema.
:param options: A dict of the key-value pairs in the ISectionSchema.
:param is_optional: A boolean. Is this section schema optional?
:raise `RedefinedKeyError`: if a keys is redefined in SectionSchema.
"""
# This method should raise RedefinedKeyError if the schema file
# redefines a key, but SafeConfigParser swallows redefined keys.
self.name = name
self._options = options
self.optional = is_optional
self.master = is_master
def __iter__(self):
"""See `ISectionSchema`"""
for key in self._options.keys():
yield key
def __contains__(self, name):
"""See `ISectionSchema`"""
return name in self._options
def __getitem__(self, key):
"""See `ISectionSchema`"""
return self._options[key]
@property
def category_and_section_names(self):
"""See `ISectionSchema`."""
if '.' in self.name:
return tuple(self.name.split('.'))
else:
return (None, self.name)
def clone(self):
"""Return a copy of this section schema."""
return self.__class__(self.name, self._options.copy(),
self.optional, self.master)
@delegate_to(ISectionSchema, context='schema')
@implementer(ISection)
class Section:
"""See `ISection`."""
def __init__(self, schema, _options=None):
"""Create an `ISection` from schema.
:param schema: The ISectionSchema that defines this ISection.
"""
# Use __dict__ because __getattr__ limits access to self.options.
self.__dict__['schema'] = schema
if _options is None:
_options = dict((key, schema[key]) for key in schema)
self.__dict__['_options'] = _options
def __getitem__(self, key):
"""See `ISection`"""
return self._options[key]
def __getattr__(self, name):
"""See `ISection`."""
if name in self._options:
return self._options[name]
else:
raise AttributeError(
"No section key named %s." % name)
def __setattr__(self, name, value):
"""Callsites cannot mutate the config by direct manipulation."""
raise AttributeError("Config options cannot be set directly.")
@property
def category_and_section_names(self):
"""See `ISection`."""
return self.schema.category_and_section_names
def update(self, items):
"""Update the keys with new values.
:return: A list of `UnknownKeyError`s if the section does not have
the key. An empty list is returned if there are no errors.
"""
errors = []
for key, value in items:
if key in self._options:
self._options[key] = value
else:
msg = "%s does not have a %s key." % (self.name, key)
errors.append(UnknownKeyError(msg))
return errors
def clone(self):
"""Return a copy of this section.
The extension mechanism requires a copy of a section to prevent
mutation.
"""
return self.__class__(self.schema, self._options.copy())
class ImplicitTypeSection(Section):
"""See `ISection`.
ImplicitTypeSection supports implicit conversion of key values to
simple datatypes. It accepts the same section data as Section; the
datatype information is not embedded in the schema or the config file.
"""
re_types = re.compile(r'''
(?P ^false$) |
(?P ^true$) |
(?P ^none$) |
(?P ^[+-]?\d+$) |
(?P ^.*)
''', re.IGNORECASE | re.VERBOSE)
def _convert(self, value):
"""Return the value as the datatype the str appears to be.
Conversion rules:
* bool: a single word, 'true' or 'false', case insensitive.
* int: a single word that is a number. Signed is supported,
hex and octal numbers are not.
* str: anything else.
"""
match = self.re_types.match(value)
if match.group('false'):
return False
elif match.group('true'):
return True
elif match.group('none'):
return None
elif match.group('int'):
return int(value)
else:
# match.group('str'); just return the sripped value.
return value.strip()
def __getitem__(self, key):
"""See `ISection`."""
value = super(ImplicitTypeSection, self).__getitem__(key)
return self._convert(value)
def __getattr__(self, name):
"""See `ISection`."""
value = super(ImplicitTypeSection, self).__getattr__(name)
return self._convert(value)
@implementer(IConfigSchema, IConfigLoader)
class ConfigSchema:
"""See `IConfigSchema`."""
_section_factory = Section
def __init__(self, filename, file_object=None):
"""Load a configuration schema from the provided filename.
:param filename: The name of the file to load from, or if
`file_object` is given, to pretend to load from.
:type filename: string
:param file_object: If given, optional file-like object to read from
instead of actually opening the named file.
:type file_object: An object with a readline() method.
:raise `UnicodeDecodeError`: if the string contains non-ascii
characters.
:raise `RedefinedSectionError`: if a SectionSchema name is redefined.
:raise `InvalidSectionNameError`: if a SectionSchema name is
ill-formed.
"""
# XXX sinzui 2007-12-13:
# RawConfigParser permits redefinition and non-ascii characters.
# The raw schema data is examined before creating a config.
self.filename = filename
self.name = basename(filename)
self._section_schemas = {}
self._category_names = []
if file_object is None:
raw_schema = self._getRawSchema(filename)
else:
raw_schema = file_object
parser = RawConfigParser()
parser.readfp(raw_schema, filename)
self._setSectionSchemasAndCategoryNames(parser)
def _getRawSchema(self, filename):
"""Return the contents of the schema at filename as a StringIO.
This method verifies that the file is ascii encoded and that no
section name is redefined.
"""
raw_schema = read_content(filename)
# Verify that the string is ascii.
raw_schema.encode('ascii', 'strict')
# Verify that no sections are redefined.
section_names = []
for section_name in re.findall(r'^\s*\[[^\]]+\]', raw_schema, re.M):
if section_name in section_names:
raise RedefinedSectionError(section_name)
else:
section_names.append(section_name)
return StringIO(raw_schema)
def _setSectionSchemasAndCategoryNames(self, parser):
"""Set the SectionSchemas and category_names from the config."""
category_names = set()
templates = {}
# Retrieve all the templates first because section() does not follow
# the order of the conf file.
for name in parser.sections():
(section_name, category_name,
is_template, is_optional,
is_master) = self._parseSectionName(name)
if is_template or is_master:
templates[category_name] = dict(parser.items(name))
for name in parser.sections():
(section_name, category_name,
is_template, is_optional,
is_master) = self._parseSectionName(name)
if is_template:
continue
options = dict(templates.get(category_name, {}))
options.update(parser.items(name))
self._section_schemas[section_name] = SectionSchema(
section_name, options, is_optional, is_master)
if category_name is not None:
category_names.add(category_name)
self._category_names = sorted(category_names)
_section_name_pattern = re.compile(r'\w[\w.-]+\w')
def _parseSectionName(self, name):
"""Return a tuple of names and kinds embedded in the name.
:return: (section_name, category_name, is_template, is_optional).
section_name is always a string. category_name is a string or
None if there is no prefix. is_template and is_optional
are False by default, but will be true if the name's suffix
ends in '.template' or '.optional'.
"""
name_parts = name.split('.')
is_template = name_parts[-1] == 'template'
is_optional = name_parts[-1] == 'optional'
is_master = name_parts[-1] == 'master'
if is_template or is_optional:
# The suffix is not a part of the section name.
# Example: [name.optional] or [category.template]
del name_parts[-1]
count = len(name_parts)
if count == 1 and is_template:
# Example: [category.template]
category_name = name_parts[0]
section_name = name_parts[0]
elif count == 1:
# Example: [name]
category_name = None
section_name = name_parts[0]
elif count == 2:
# Example: [category.name]
category_name = name_parts[0]
section_name = '.'.join(name_parts)
else:
raise InvalidSectionNameError('[%s] has too many parts.' % name)
if self._section_name_pattern.match(section_name) is None:
raise InvalidSectionNameError(
'[%s] name does not match [\w.-]+.' % name)
return (section_name, category_name,
is_template, is_optional, is_master)
@property
def section_factory(self):
"""See `IConfigSchema`."""
return self._section_factory
@property
def category_names(self):
"""See `IConfigSchema`."""
return self._category_names
def __iter__(self):
"""See `IConfigSchema`."""
for value in self._section_schemas.values():
yield value
def __contains__(self, name):
"""See `IConfigSchema`."""
return name in self._section_schemas.keys()
def __getitem__(self, name):
"""See `IConfigSchema`."""
try:
return self._section_schemas[name]
except KeyError:
raise NoSectionError(name)
def getByCategory(self, name, default=_missing):
"""See `IConfigSchema`."""
if name not in self.category_names:
if default is _missing:
raise NoCategoryError(name)
return default
section_schemas = []
for key in self._section_schemas:
section = self._section_schemas[key]
category, dummy = section.category_and_section_names
if name == category:
section_schemas.append(section)
return section_schemas
def _getRequiredSections(self):
"""return a dict of `Section`s from the required `SectionSchemas`."""
sections = {}
for section_schema in self:
if not section_schema.optional:
sections[section_schema.name] = self.section_factory(
section_schema)
return sections
def load(self, filename):
"""See `IConfigLoader`."""
conf_data = read_content(filename)
return self._load(filename, conf_data)
def loadFile(self, source_file, filename=None):
"""See `IConfigLoader`."""
conf_data = source_file.read()
if filename is None:
filename = getattr(source_file, 'name')
assert filename is not None, (
'filename must be provided if the file-like object '
'does not have a name attribute.')
return self._load(filename, conf_data)
def _load(self, filename, conf_data):
"""Return a Config parsed from conf_data."""
config = Config(self)
config.push(filename, conf_data)
return config
class ImplicitTypeSchema(ConfigSchema):
"""See `IConfigSchema`.
ImplicitTypeSchema creates a config that supports implicit datatyping
of section key values.
"""
_section_factory = ImplicitTypeSection
@implementer(IConfigData)
class ConfigData:
"""See `IConfigData`."""
def __init__(self, filename, sections, extends=None, errors=None):
"""Set the configuration data."""
self.filename = filename
self.name = basename(filename)
self._sections = sections
self._category_names = self._getCategoryNames()
self._extends = extends
if errors is None:
self._errors = []
else:
self._errors = errors
def _getCategoryNames(self):
"""Return a tuple of category names that the `Section`s belong to."""
category_names = set()
for section_name in self._sections:
section = self._sections[section_name]
category, dummy = section.category_and_section_names
if category is not None:
category_names.add(category)
return tuple(category_names)
@property
def category_names(self):
"""See `IConfigData`."""
return self._category_names
def __iter__(self):
"""See `IConfigData`."""
for value in self._sections.values():
yield value
def __contains__(self, name):
"""See `IConfigData`."""
return name in self._sections.keys()
def __getitem__(self, name):
"""See `IConfigData`."""
try:
return self._sections[name]
except KeyError:
raise NoSectionError(name)
def getByCategory(self, name, default=_missing):
"""See `IConfigData`."""
if name not in self.category_names:
if default is _missing:
raise NoCategoryError(name)
return default
sections = []
for key in self._sections:
section = self._sections[key]
category, dummy = section.category_and_section_names
if name == category:
sections.append(section)
return sections
@delegate_to(IConfigData, context='data')
@implementer(IStackableConfig)
class Config:
"""See `IStackableConfig`."""
# LAZR config classes may access ConfigData private data.
# pylint: disable-msg=W0212
def __init__(self, schema):
"""Set the schema and configuration."""
self._overlays = (
ConfigData(schema.filename, schema._getRequiredSections()), )
self.schema = schema
def __getattr__(self, name):
"""See `IStackableConfig`."""
if name in self.data._sections:
return self.data._sections[name]
elif name in self.data._category_names:
return Category(name, self.data.getByCategory(name))
raise AttributeError("No section or category named %s." % name)
@property
def data(self):
"""See `IStackableConfig`."""
return self.overlays[0]
@property
def extends(self):
"""See `IStackableConfig`."""
if len(self.overlays) == 1:
# The ConfigData made from the schema defaults extends nothing.
return None
else:
return self.overlays[1]
@property
def overlays(self):
"""See `IStackableConfig`."""
return self._overlays
def validate(self):
"""See `IConfigData`."""
if len(self.data._errors) > 0:
message = "%s is not valid." % self.name
raise ConfigErrors(message, errors=self.data._errors)
return True
def push(self, conf_name, conf_data):
"""See `IStackableConfig`.
Create a new ConfigData object from the raw conf_data, and
place it on top of the overlay stack. If the conf_data extends
another conf, a ConfigData object will be created for that first.
"""
conf_data = dedent(conf_data)
confs = self._getExtendedConfs(conf_name, conf_data)
confs.reverse()
for conf_name, parser, encoding_errors in confs:
if self.data.filename == self.schema.filename == conf_name:
# Do not parse the schema file twice in a row.
continue
config_data = self._createConfigData(
conf_name, parser, encoding_errors)
self._overlays = (config_data, ) + self._overlays
def _getExtendedConfs(self, conf_filename, conf_data, confs=None):
"""Return a list of tuple (conf_name, parser, encoding_errors).
:param conf_filename: The path and name of the conf file.
:param conf_data: Unparsed config data.
:param confs: A list of confs that extend filename.
:return: A list of confs ordered from extender to extendee.
:raises IOError: If filename cannot be read.
This method parses the config data and checks for encoding errors.
It checks parsed config data for the extends key in the meta section.
It reads the unparsed config_data from the extended filename.
It passes filename, data, and the working list to itself.
"""
if confs is None:
confs = []
encoding_errors = self._verifyEncoding(conf_data)
# LP: #1397779. In Python 3, RawConfigParser grew a `strict` keyword
# option and in Python 3.2, this argument changed its default from
# False to True. This breaks behavior compatibility with Python 2, so
# under Python 3, always force strict=False.
kws = {}
if sys.version_info >= (3,):
kws['strict'] = False
parser = RawConfigParser(**kws)
parser.readfp(StringIO(conf_data), conf_filename)
confs.append((conf_filename, parser, encoding_errors))
if parser.has_option('meta', 'extends'):
base_path = dirname(conf_filename)
extends_name = parser.get('meta', 'extends')
extends_filename = abspath('%s/%s' % (base_path, extends_name))
extends_data = read_content(extends_filename)
self._getExtendedConfs(extends_filename, extends_data, confs)
return confs
def _createConfigData(self, conf_name, parser, encoding_errors):
"""Return a new ConfigData object created from a parsed conf file.
:param conf_name: the full name of the config file, may be a filepath.
:param parser: the parsed config file; an instance of ConfigParser.
:param encoding_errors: a list of encoding error in the config file.
:return: a new ConfigData object.
This method extracts the sections, keys, and values from the parser
to construct a new ConfigData object. The list of encoding errors are
incorporated into the the list of data-related errors for the
ConfigData.
"""
sections = {}
for section in self.data:
sections[section.name] = section.clone()
errors = list(self.data._errors)
errors.extend(encoding_errors)
extends = None
masters = set()
for section_name in parser.sections():
if section_name == 'meta':
extends, meta_errors = self._loadMetaData(parser)
errors.extend(meta_errors)
continue
if (section_name.endswith('.template') or
section_name.endswith('.optional') or
section_name.endswith('.master')):
# This section is a schema directive.
continue
# Calculate the section master name.
# Check for sections which extend .masters.
if '.' in section_name:
category, section = section_name.split('.')
master_name = category + '.master'
else:
master_name = None
if (section_name not in self.schema and
master_name not in self.schema):
# Any section not in the the schema is an error.
msg = "%s does not have a %s section." % (
self.schema.name, section_name)
errors.append(UnknownSectionError(msg))
continue
if section_name not in self.data:
# Is there a master section?
try:
section_schema = self.schema[master_name]
except NoSectionError:
# There's no master for this section, so just treat it
# like a regular category.
pass
else:
assert section_schema.master, '.master is not a master?'
schema = section_schema.clone()
schema.name = section_name
section = self.schema.section_factory(schema)
section.update(parser.items(section_name))
sections[section_name] = section
masters.add(master_name)
continue
# Create the optional section from the schema.
section_schema = self.schema[section_name]
sections[section_name] = self.schema.section_factory(
section_schema)
# Update the section with the parser options.
items = parser.items(section_name)
section_errors = sections[section_name].update(items)
errors.extend(section_errors)
# master sections are like templates. They show up in the schema but
# not in the config.
for master in masters:
sections.pop(master, None)
return ConfigData(conf_name, sections, extends, errors)
def _verifyEncoding(self, config_data):
"""Verify that the data is ASCII encoded.
:return: a list of UnicodeDecodeError errors. If there are no
errors, return an empty list.
"""
errors = []
try:
if isinstance(config_data, bytes):
config_data.decode('ascii', 'strict')
else:
config_data.encode('ascii', 'strict')
except UnicodeError as error:
errors.append(error)
return errors
def _loadMetaData(self, parser):
"""Load the config meta data from the ConfigParser.
The meta section is reserved for the LAZR config parser.
:return: a list of errors if there are errors, or an empty list.
"""
extends = None
errors = []
for key in parser.options('meta'):
if key == "extends":
extends = parser.get('meta', 'extends')
else:
# Any other key is an error.
msg = "The meta section does not have a %s key." % key
errors.append(UnknownKeyError(msg))
return (extends, errors)
def pop(self, conf_name):
"""See `IStackableConfig`."""
index = self._getIndexOfOverlay(conf_name)
removed_overlays = self.overlays[:index]
self._overlays = self.overlays[index:]
return removed_overlays
def _getIndexOfOverlay(self, conf_name):
"""Return the index of the config named conf_name.
The bottom of the stack cannot never be returned because it was
made from the schema.
"""
schema_index = len(self.overlays) - 1
for index, config_data in enumerate(self.overlays):
if index == schema_index and config_data.name == conf_name:
raise NoConfigError("Cannot pop the schema's default config.")
if config_data.name == conf_name:
return index + 1
# The config data was not found in the overlays.
raise NoConfigError('No config with name: %s.' % conf_name)
@implementer(ICategory)
class Category:
"""See `ICategory`."""
def __init__(self, name, sections):
"""Initialize the Category its name and a list of sections."""
self.name = name
self._sections = {}
for section in sections:
self._sections[section.name] = section
def __getattr__(self, name):
"""See `ICategory`."""
full_name = "%s.%s" % (self.name, name)
if full_name in self._sections:
return self._sections[full_name]
raise AttributeError("No section named %s." % name)
def as_boolean(value):
"""Turn a string into a boolean.
:param value: A string with one of the following values
(case-insensitive): true, yes, 1, on, enable, enabled (for True), or
false, no, 0, off, disable, disabled (for False). Everything else is
an error.
:type value: string
:return: True or False.
:rtype: boolean
"""
value = value.lower()
if value in ('true', 'yes', '1', 'on', 'enabled', 'enable'):
return True
if value in ('false', 'no', '0', 'off', 'disabled', 'disable'):
return False
raise ValueError('Invalid boolean value: %s' % value)
def as_host_port(value, default_host='localhost', default_port=25):
"""Return a 2-tuple of (host, port) from a value like 'host:port'.
:param value: The configuration value.
:type value: string
:param default_host: Optional host name to use if the configuration value
is missing the host name.
:type default_host: string
:param default_port: Optional port number to use if the configuration
value is missing the port number.
:type default_port: integer
:return: a 2-tuple of the form (host, port)
:rtype: 2-tuple of (string, integer)
"""
if ':' in value:
host, port = value.split(':')
if host == '':
host = default_host
port = int(port)
else:
host = value
port = default_port
return host, port
def as_username_groupname(value=None):
"""Turn a string of the form user:group into the user and group names.
:param value: The configuration value.
:type value: a string containing exactly one colon, or None
:return: a 2-tuple of (username, groupname). If `value` was None, then
the current user and group names are returned.
:rtype: 2-tuple of type (string, string)
"""
if value:
user, group = value.split(':', 1)
else:
user = pwd.getpwuid(os.getuid()).pw_name
group = grp.getgrgid(os.getgid()).gr_name
return user, group
def _sortkey(item):
"""Return a value that sorted(..., key=_sortkey) can use."""
order = dict(
w=0, # weeks
d=1, # days
h=2, # hours
m=3, # minutes
s=4, # seconds
)
return order.get(item[-1])
def as_timedelta(value):
"""Convert a value string to the equivalent timedeta."""
# Technically, the regex will match multiple decimal points in the
# left-hand side, but that's okay because the float/int conversion below
# will properly complain if there's more than one dot.
components = sorted(re.findall(r'([\d.]+[smhdw])', value), key=_sortkey)
# Complain if the components are out of order.
if ''.join(components) != value:
raise ValueError
keywords = dict((interval[0].lower(), interval)
for interval in ('weeks', 'days', 'hours',
'minutes', 'seconds'))
keyword_arguments = {}
for interval in components:
if len(interval) == 0:
raise ValueError
keyword = keywords.get(interval[-1].lower())
if keyword is None:
raise ValueError
if keyword in keyword_arguments:
raise ValueError
if '.' in interval[:-1]:
converted = float(interval[:-1])
else:
converted = int(interval[:-1])
keyword_arguments[keyword] = converted
if len(keyword_arguments) == 0:
raise ValueError
return datetime.timedelta(**keyword_arguments)
def as_log_level(value):
"""Turn a string into a log level.
:param value: A string with a value (case-insensitive) equal to one of the
symbolic logging levels.
:type value: string
:return: A logging level constant.
:rtype: int
"""
value = value.upper()
return getattr(logging, value)
lazr.config-2.1/lazr/config/interfaces.py 0000664 0001750 0001750 00000020746 12452627511 021002 0 ustar barry barry 0000000 0000000 # Copyright 2007-2015 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.config
#
# lazr.config is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.config is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.config. If not, see .
# pylint: disable-msg=E0211,E0213,W0231
"""Interfaces for process configuration.."""
from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'ConfigErrors',
'ConfigSchemaError',
'IConfigData',
'NoConfigError',
'ICategory',
'IConfigLoader',
'IConfigSchema',
'InvalidSectionNameError',
'ISection',
'ISectionSchema',
'IStackableConfig',
'NoCategoryError',
'RedefinedKeyError',
'RedefinedSectionError',
'UnknownKeyError',
'UnknownSectionError']
from zope.interface import Interface, Attribute
class ConfigSchemaError(Exception):
"""A base class of all `IConfigSchema` errors."""
class RedefinedKeyError(ConfigSchemaError):
"""A key in a section cannot be redefined."""
class RedefinedSectionError(ConfigSchemaError):
"""A section in a config file cannot be redefined."""
class InvalidSectionNameError(ConfigSchemaError):
"""The section name contains more than one category."""
class NoCategoryError(LookupError):
"""No `ISectionSchema`s belong to the category name."""
class UnknownSectionError(ConfigSchemaError):
"""The config has a section that is not in the schema."""
class UnknownKeyError(ConfigSchemaError):
"""The section has a key that is not in the schema."""
class NoConfigError(ConfigSchemaError):
"""No config has the name."""
class ConfigErrors(ConfigSchemaError):
"""The errors in a Config.
The list of errors can be accessed via the errors attribute.
"""
def __init__(self, message, errors=None):
"""Initialize the error with a message and errors.
:param message: a message string
:param errors: a list of errors in the config, or None
"""
# Without the suppression above, this produces a warning in Python 2.6.
self.message = message
self.errors = errors
def __str__(self):
return '%s: %s' % (self.__class__.__name__, self.message)
class ISectionSchema(Interface):
"""Defines the valid keys and default values for a configuration group."""
name = Attribute("The section name.")
optional = Attribute("Is the section optional in the config?")
category_and_section_names = Attribute(
"A 2-Tuple of the category and specific name parts.")
def __iter__():
"""Iterate over the keys."""
def __contains__(name):
"""Return True or False if name is a key."""
def __getitem__(key):
"""Return the default value of the key.
:raise `KeyError`: if the key does not exist.
"""
class ISection(ISectionSchema):
"""Defines the values for a configuration group."""
schema = Attribute("The ISectionSchema that defines this ISection.")
def __getattr__(name):
"""Return the named key.
:name: a key name.
:return: the value of the matching key.
:raise: AttributeError if there is no key with the name.
"""
class IConfigLoader(Interface):
"""A configuration file loader."""
def load(filename):
"""Load a configuration from the file at filename."""
def loadFile(source_file, filename=None):
"""Load a configuration from the open source_file.
:param source_file: A file-like object that supports read() and
readline()
:param filename: The name of the configuration. If filename is None,
The name will be taken from source_file.name.
"""
class IConfigSchema(Interface):
"""A process configuration schema.
The config file contains sections enclosed in square brackets ([]).
The section name may be divided into major and minor categories using a
dot (.). Beneath each section is a list of key-value pairs, separated
by a colon (:).
Multiple sections with the same major category may have their keys defined
in another section that appends the '.template' or '.master' suffixes to
the category name. A section with '.optional' suffix is not
required. Lines that start with a hash (#) are comments.
"""
name = Attribute('The basename of the config filename.')
filename = Attribute('The path to config file')
category_names = Attribute('The list of section category names.')
def __iter__():
"""Iterate over the `ISectionSchema`s."""
def __contains__(name):
"""Return True or False if the name matches a `ISectionSchema`."""
def __getitem__(name):
"""Return the `ISectionSchema` with the matching name.
:raise `NoSectionError`: if the no ISectionSchema has the name.
"""
def getByCategory(name):
"""Return a list of ISectionSchemas that belong to the category name.
`ISectionSchema` names may be made from a category name and a group
name, separated by a dot (.). The category is synonymous with a
arbitrary resource such as a database or a vhost. Thus database.bugs
and database.answers are two sections that both use the database
resource.
:raise `CategoryNotFound`: if no sections have a name that starts
with the category name.
"""
class IConfigData(IConfigSchema):
"""A process configuration.
See `IConfigSchema` for more information about the config file format.
"""
class IStackableConfig(IConfigSchema):
"""A configuration that is built from configs that extend each other.
A config may extend another config so that a configuration for a
process need only define the localized sections and keys. The
configuration is constructed from a stack of data that defines,
and redefines, the sections and keys in the configuration. Each config
overlays its data to define the final configuration.
A config file declares that is extends another using the 'extends' key
in the 'meta' section of the config data file:
[meta]
extends: common.conf
The push() and pop() methods can be used to test processes where the
test environment must be configured differently.
"""
schema = Attribute("The schema that defines the config.")
data = Attribute("The current ConfigData. use by the config.")
extends = Attribute("The ConfigData that this config extends.")
overlays = Attribute("The stack of ConfigData that define this config.")
def __getattr__(name):
"""Return the named section.
:name: a section or category name.
:return: the matching `ISection` or `ICategory`.
:raise: AttributeError if there is no section or category with the
name.
"""
def validate():
"""Return True if the config is valid for the schema.
:raise `ConfigErrors`: if the are errors. A list of all schema
problems can be retrieved via the errors property.
"""
def push(conf_name, conf_data):
"""Overlay the config with unparsed config data.
:param conf_name: the name of the config.
:param conf_data: a string of unparsed config data.
This method appends the parsed `IConfigData` to the overlays property.
"""
def pop(conf_name):
"""Remove conf_name from the overlays stack.
:param conf_name: the name of the `IConfigData` to remove.
:return: the tuple of `IConfigData` that was removed from overlays.
:raise NoConfigError: if no `IConfigData` has the conf_name.
This method removes the named ConfigData from the stack; ConfigData
above the named ConfigData are removed too.
"""
class ICategory(Interface):
"""A group of related sections.
The sections within a category are access as attributes of the
`ICategory`.
"""
def __getattr__(name):
"""Return the named section.
:name: a section name.
:return: the matching `ISection`.
:raise: AttributeError if there is no section with the name.
"""
lazr.config-2.1/lazr/config/version.txt 0000664 0001750 0001750 00000000004 12452627637 020525 0 ustar barry barry 0000000 0000000 2.1
lazr.config-2.1/lazr/config/tests/ 0000775 0001750 0001750 00000000000 12452641133 017432 5 ustar barry barry 0000000 0000000 lazr.config-2.1/lazr/config/tests/test_config.py 0000664 0001750 0001750 00000017421 12452635442 022323 0 ustar barry barry 0000000 0000000 # Copyright 2008-2015 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.config.
#
# lazr.config is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.config is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.config. If not, see .
"""Tests of lazr.config."""
from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'TestConfig',
]
import unittest
import pkg_resources
try:
from configparser import MissingSectionHeaderError, NoSectionError
except ImportError:
# Python 2
from ConfigParser import MissingSectionHeaderError, NoSectionError
try:
from io import StringIO
except ImportError:
# Python 2
from StringIO import StringIO
from operator import attrgetter
from zope.interface.exceptions import DoesNotImplement
from zope.interface.verify import verifyObject
from lazr.config import ConfigSchema, ImplicitTypeSchema
from lazr.config.interfaces import (
ConfigErrors, IStackableConfig, InvalidSectionNameError, NoCategoryError,
NoConfigError, RedefinedSectionError, UnknownKeyError,
UnknownSectionError)
class TestConfig(unittest.TestCase):
def setUp(self):
# Python 2.6 does not have assertMultilineEqual
self.meq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
def _testfile(self, conf_file):
return pkg_resources.resource_filename(
'lazr.config.tests.testdata', conf_file)
def test_missing_category(self):
schema = ConfigSchema(self._testfile('base.conf'))
self.assertRaises(NoCategoryError, schema.getByCategory, 'non-section')
def test_missing_file(self):
self.assertRaises(IOError, ConfigSchema, '/does/not/exist')
def test_must_be_ascii(self):
self.assertRaises(UnicodeError,
ConfigSchema, self._testfile('bad-nonascii.conf'))
def test_missing_schema_section(self):
schema = ConfigSchema(self._testfile('base.conf'))
self.assertRaises(NoSectionError, schema.__getitem__, 'section-4')
def test_missing_header_section(self):
self.assertRaises(MissingSectionHeaderError,
ConfigSchema, self._testfile('bad-sectionless.conf'))
def test_redefined_section(self):
self.assertRaises(RedefinedSectionError,
ConfigSchema,
self._testfile('bad-redefined-section.conf'))
# XXX sinzui 2007-12-13:
# ConfigSchema should raise RedefinedKeyError when a section redefines
# a key.
def test_invalid_section_name(self):
self.assertRaises(InvalidSectionNameError,
ConfigSchema,
self._testfile('bad-invalid-name.conf'))
def test_invalid_characters(self):
self.assertRaises(InvalidSectionNameError,
ConfigSchema,
self._testfile('bad-invalid-name-chars.conf'))
def test_load_missing_file(self):
schema = ConfigSchema(self._testfile('base.conf'))
self.assertRaises(IOError, schema.load, '/no/such/file.conf')
def test_no_name_argument(self):
config = """
[meta]
metakey: unsupported
[unknown-section]
key1 = value1
[section_1]
keyn: unknown key
key1: bad character in caf\xc3)
[section_3.template]
key1: schema suffixes are not permitted
"""
schema = ConfigSchema(self._testfile('base.conf'))
self.assertRaises(AttributeError, schema.loadFile, StringIO(config))
def test_missing_section(self):
schema = ConfigSchema(self._testfile('base.conf'))
config = schema.load(self._testfile('local.conf'))
self.assertRaises(NoSectionError, config.__getitem__, 'section-4')
def test_undeclared_optional_section(self):
schema = ConfigSchema(self._testfile('base.conf'))
config = schema.load(self._testfile('local.conf'))
self.assertRaises(NoSectionError,
config.__getitem__, 'section_3.app_a')
def test_nonexistent_category_name(self):
schema = ConfigSchema(self._testfile('base.conf'))
config = schema.load(self._testfile('local.conf'))
self.assertRaises(NoCategoryError,
config.getByCategory, 'non-section')
def test_all_config_errors(self):
schema = ConfigSchema(self._testfile('base.conf'))
config = schema.loadFile(StringIO("""
[meta]
metakey: unsupported
[unknown-section]
key1 = value1
[section_1]
keyn: unknown key
key1: bad character in caf\xc3)
[section_3.template]
key1: schema suffixes are not permitted
"""), 'bad config')
try:
config.validate()
except ConfigErrors as errors:
sorted_errors = sorted(
errors.errors, key=attrgetter('__class__.__name__'))
self.assertEqual(str(errors),
'ConfigErrors: bad config is not valid.')
else:
self.fail('ConfigErrors expected')
self.assertEqual(len(sorted_errors), 4)
self.assertEqual([error.__class__ for error in sorted_errors],
[UnicodeEncodeError, UnknownKeyError,
UnknownKeyError, UnknownSectionError])
def test_not_stackable(self):
schema = ConfigSchema(self._testfile('base.conf'))
config = schema.load(self._testfile('local.conf'))
self.assertRaises(DoesNotImplement,
verifyObject, IStackableConfig, config.extends)
def test_bad_pop(self):
schema = ConfigSchema(self._testfile('base.conf'))
config = schema.load(self._testfile('local.conf'))
config.push('one', '')
config.push('two', '')
self.assertRaises(NoConfigError, config.pop, 'bad-name')
def test_cannot_pop_bottom(self):
schema = ConfigSchema(self._testfile('base.conf'))
config = schema.load(self._testfile('local.conf'))
config.pop('local.conf')
self.assertRaises(NoConfigError, config.pop, 'base.conf')
def test_multiline_preserves_indentation(self):
schema = ImplicitTypeSchema(self._testfile('base.conf'))
config = schema.load(self._testfile('local.conf'))
convert = config['section_1']._convert
orig = """\
multiline value 1
multiline value 2"""
new = convert(orig)
self.meq(new, orig)
def test_multiline_strips_leading_and_trailing_whitespace(self):
schema = ImplicitTypeSchema(self._testfile('base.conf'))
config = schema.load(self._testfile('local.conf'))
convert = config['section_1']._convert
orig = """
multiline value 1
multiline value 2
"""
new = convert(orig)
self.meq(new, orig.strip())
def test_multiline_key(self):
schema = ImplicitTypeSchema(self._testfile('base.conf'))
config = schema.load(self._testfile('local.conf'))
self.meq(config['section_33'].key2, """\
multiline value 1
multiline value 2""")
def test_lp1397779(self):
# Fix DuplicateSectionErrors when you .push() a config that has a
# section already defined in the config.
schema = ConfigSchema(self._testfile('base.conf'))
config = schema.load(self._testfile('local.conf'))
self.assertEqual(config['section_1']['key1'], 'foo')
config.push('dupsec', """\
[section_1]
key1: baz
[section_1]
key1: qux
""")
self.assertEqual(config['section_1']['key1'], 'qux')
lazr.config-2.1/lazr/config/tests/testdata/ 0000775 0001750 0001750 00000000000 12452641133 021243 5 ustar barry barry 0000000 0000000 lazr.config-2.1/lazr/config/tests/testdata/shared.conf 0000664 0001750 0001750 00000000174 12452627145 023371 0 ustar barry barry 0000000 0000000 # The schema is defined by base.conf.
# Localize a key for section_1.
[section_1]
key2: sharing is fun
key5: shared value
lazr.config-2.1/lazr/config/tests/testdata/base.conf 0000664 0001750 0001750 00000001455 12452627145 023040 0 ustar barry barry 0000000 0000000 # This section defines required keys and default values.
[section_1]
key1: foo
key2: bar and baz
key3: Launchpad rocks
key4: Fc;k yeah!
key5:
# This section is required, and it defines all the keys for its category.
[section-2.app-b]
key1: True
# This section is optional; it uses the keys defined
# by section_3.template.
[section_3.app_a.optional]
# This is a required section whose keys are defined by section_3.template
# and it defines a new key.
[section_3.app_b]
key2: changed
key3: unique
# These sections define a common set of required keys and default values.
[section_3.template]
key1: 17
key2: 3.1415
# This section is optional.
[section-5.optional]
key1: something
# This section has a name similar to a category.
[section_33]
key1: fnord
key2: multiline value 1
multiline value 2
lazr.config-2.1/lazr/config/tests/testdata/bad-nonascii.conf 0000664 0001750 0001750 00000000070 12452627145 024445 0 ustar barry barry 0000000 0000000 # Non ascii character: é.
[test-section]
key1: café.
lazr.config-2.1/lazr/config/tests/testdata/bad-invalid-name-chars.conf 0000664 0001750 0001750 00000000106 12452627145 026304 0 ustar barry barry 0000000 0000000 # This section name is invalid
[$category.name_part.optional]
key1: 0
lazr.config-2.1/lazr/config/tests/testdata/bad-invalid-name.conf 0000664 0001750 0001750 00000000117 12452627145 025210 0 ustar barry barry 0000000 0000000 # This section name is invalid
[category.other_category.name.optional]
key1: 0
lazr.config-2.1/lazr/config/tests/testdata/master.conf 0000664 0001750 0001750 00000000171 12452627145 023413 0 ustar barry barry 0000000 0000000 # This section defines a category master.
[thing.master]
foo: aardvark
[bar.master]
baz: badger
[bar.soup]
baz: cougar
lazr.config-2.1/lazr/config/tests/testdata/local.conf 0000664 0001750 0001750 00000000240 12452627145 023207 0 ustar barry barry 0000000 0000000 [meta]
extends: shared.conf
# Localize a key for section_1.
[section_1]
key5: local value
# Accept the default values for the optional section-5.
[section-5]
lazr.config-2.1/lazr/config/tests/testdata/bad-redefined-key.conf 0000664 0001750 0001750 00000000077 12452627145 025364 0 ustar barry barry 0000000 0000000 # Redefined key
[test-section]
key1: original
key1: redefined
lazr.config-2.1/lazr/config/tests/testdata/master-local.conf 0000664 0001750 0001750 00000000126 12452627145 024503 0 ustar barry barry 0000000 0000000 # Define a few categories based on the master.
[thing.one]
foo: 1
[thing.two]
foo: 2
lazr.config-2.1/lazr/config/tests/testdata/__init__.py 0000664 0001750 0001750 00000000000 12452627145 023351 0 ustar barry barry 0000000 0000000 lazr.config-2.1/lazr/config/tests/testdata/bad-redefined-section.conf 0000664 0001750 0001750 00000000150 12452627145 026230 0 ustar barry barry 0000000 0000000 [test-section]
key1: original
key2: redefined
#Redefine the test-section
[test-section]
key3: a value
lazr.config-2.1/lazr/config/tests/testdata/bad-sectionless.conf 0000664 0001750 0001750 00000000062 12452627145 025176 0 ustar barry barry 0000000 0000000 orphaned-key: value
[test-section]
key1 = option
lazr.config-2.1/lazr/config/tests/__init__.py 0000664 0001750 0001750 00000000000 12452627145 021540 0 ustar barry barry 0000000 0000000 lazr.config-2.1/lazr/config/__init__.py 0000664 0001750 0001750 00000002333 12452627461 020412 0 ustar barry barry 0000000 0000000 # Copyright 2007-2015 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.config
#
# lazr.config is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.config is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.config. If not, see .
"""A configuration file system."""
import pkg_resources
__version__ = pkg_resources.resource_string(
"lazr.config", "version.txt").strip()
# Re-export in such a way that __version__ can still be imported if
# dependencies are not yet available.
try:
# While we generally frown on "*" imports, this, combined with the fact we
# only test code from this module, means that we can verify what has been
# exported.
from lazr.config._config import *
from lazr.config._config import __all__
except ImportError:
pass
lazr.config-2.1/lazr/config/docs/ 0000775 0001750 0001750 00000000000 12452641133 017220 5 ustar barry barry 0000000 0000000 lazr.config-2.1/lazr/config/docs/usage_fixture.py 0000664 0001750 0001750 00000001642 12452627504 022455 0 ustar barry barry 0000000 0000000 # Copyright 2009-2015 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.smtptest
#
# lazr.smtptest is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.smtptest is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.smtptest. If not, see .
"""Doctest fixtures for running under nose."""
from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'globs',
]
from lazr.config.docs.fixture import globs
lazr.config-2.1/lazr/config/docs/NEWS.rst 0000664 0001750 0001750 00000003764 12452640447 020547 0 ustar barry barry 0000000 0000000 ====================
NEWS for lazr.config
====================
2.1 (2015-01-05)
================
- Always use old-style namespace package registration in ``lazr/__init__.py``
since the mere presence of this file subverts PEP 420 style namespace
packages. (LP: #1407816)
- For behavioral compatibility between Python 2 and 3, `strict=False` must be
passed to the underlying `RawConfigParser` under Python 3. (LP: #1397779)
2.0.1 (2014-08-22)
==================
- Drop the use of `distribute` in favor of `setuptools`. (LP: #1359926)
- Run the test suite with `tox`.
2.0 (2013-01-10)
================
- Ported to Python 3.
- Now more strict in its requirement of ASCII in config files.
- Category names are now sorted by default.
1.1.3 (2009-08-25)
==================
- Fixed a build problem.
1.1.2 (2009-08-25)
==================
- Got rid of a sys.path hack.
1.1.1 (2009-03-24)
==================
- License clarification: only v3 of the LGPL is offered at this time, not
subsequent versions.
- Build is updated to support Sphinx docs and other small changes.
1.1 (2009-01-05)
================
- Support for adding arbitrary sections in a configuration file, based on a
.master section in the schema. The .master section allows admins to define
configurations for an arbitrary number of processes. If the schema defines
.master sections, then the conf file can contain sections that extend the
.master section. These are like categories with templates except that the
section names extending .master need not be named in the schema file.
[Bug 310619]
- ConfigSchema now provides an interface for constructing the schema from a
string. [Bug 309859]
- Added as_boolean() and as_log_level() type converters. [Bug 310782]
- getByCategory() accepts a default argument. If the category is missing, the
default argument is returned. If the category is missing and no default
argument is given, a NoCategoryError is raised, as before. [Bug 309988]
1.0 (2008-12-19)
================
- Initial release
lazr.config-2.1/lazr/config/docs/__init__.py 0000664 0001750 0001750 00000000000 12452627145 021326 0 ustar barry barry 0000000 0000000 lazr.config-2.1/lazr/config/docs/usage.rst 0000664 0001750 0001750 00000114377 12452640753 021102 0 ustar barry barry 0000000 0000000 ===========
LAZR config
===========
The LAZR config system is typically used to manage process configuration.
Process configuration is for saying how things change when we run systems on
different machines, or under different circumstances.
This system uses ini-like file format of section, keys, and values. The
config file supports inheritance to minimize duplication of information across
files. The format supports schema validation.
ConfigSchema
============
A schema is loaded by instantiating the ConfigSchema class with the path to a
configuration file. The schema is explicitly derived from the information in
the configuration file.
>>> from pkg_resources import resource_string
>>> raw_schema = resource_string('lazr.config.tests.testdata', 'base.conf')
The config file contains sections enclosed in square brackets
(e.g. ``[section]``). The section name may be divided into major and minor
categories using a dot (``.``). Beneath each section is a list of key-value
pairs, separated by a colon (``:``).
Multiple sections with the same major category may have their keys defined in
another section that appends the ``.template`` suffix to the category name.
A section with ``.optional`` suffix is not required. Lines that start with a
hash (``#``) are comments.
>>> from pkg_resources import resource_string
>>> raw_schema = resource_string('lazr.config.tests.testdata', 'base.conf')
>>> print(raw_schema.decode('utf-8'))
# This section defines required keys and default values.
[section_1]
key1: foo
key2: bar and baz
key3: Launchpad rocks
key4: Fc;k yeah!
key5:
# This section is required, and it defines all the keys for its category.
[section-2.app-b]
key1: True
# This section is optional; it uses the keys defined
# by section_3.template.
[section_3.app_a.optional]
# This is a required section whose keys are defined by section_3.template
# and it defines a new key.
[section_3.app_b]
key2: changed
key3: unique
# These sections define a common set of required keys and default values.
[section_3.template]
key1: 17
key2: 3.1415
# This section is optional.
[section-5.optional]
key1: something
# This section has a name similar to a category.
[section_33]
key1: fnord
key2: multiline value 1
multiline value 2
To create the schema, provide a file name.
>>> from lazr.config import ConfigSchema
>>> from lazr.config.interfaces import IConfigSchema
>>> from pkg_resources import resource_filename
>>> from zope.interface.verify import verifyObject
>>> base_conf = resource_filename(
... 'lazr.config.tests.testdata', 'base.conf')
>>> schema = ConfigSchema(base_conf)
>>> verifyObject(IConfigSchema, schema)
True
The schema has a name and a file name.
>>> print(schema.name)
base.conf
>>> print('file:', schema.filename)
file: ...lazr/config/tests/testdata/base.conf
If you provide an optional file-like object as a second argument to the
constructor, that is used instead of opening the named file implicitly.
>>> with open(base_conf, 'r') as file_object:
... other_schema = ConfigSchema('/does/not/exist.conf', file_object)
>>> verifyObject(IConfigSchema, other_schema)
True
For such schemas, the file name is taken from the first argument.
>>> print(other_schema.name)
exist.conf
>>> print(other_schema.filename)
/does/not/exist.conf
A schema is made up of multiple SchemaSections. They can be iterated
over in a loop as needed.
>>> from operator import attrgetter
>>> for section_schema in sorted(schema, key=attrgetter('name')):
... print(section_schema.name)
section-2.app-b
section-5
section_1
section_3.app_a
section_3.app_b
section_33
>>> for section_schema in sorted(other_schema, key=attrgetter('name')):
... print(section_schema.name)
section-2.app-b
section-5
section_1
section_3.app_a
section_3.app_b
section_33
You can check if the schema contains a section name, and that can be
used to access the SchemaSection as a subscript.
>>> 'section_1' in schema
True
>>> 'section-4' in schema
False
A SectionSchema can be retrieved from the schema using the ``[]`` operator.
>>> section_schema_1 = schema['section_1']
>>> print(section_schema_1.name)
section_1
Processes often require resources like databases or virtual hosts that have a
common category of keys. The list of all category names can be retrieved via
the categories attribute.
>>> for name in schema.category_names:
... print(name)
section-2
section_3
The list of SchemaSections that share common category can be retrieved
using ``getByCategory()``.
>>> all_section_3 = schema.getByCategory('section_3')
>>> for section_schema in sorted(all_section_3, key=attrgetter('name')):
... print(section_schema.name)
section_3.app_a
section_3.app_b
You can pass a default argument to ``getByCategory()`` to avoid the exception.
>>> missing = object()
>>> schema.getByCategory('non-section', missing) is missing
True
SchemaSection
=============
A SchemaSection behaves similar to a dictionary. It has keys and values.
>>> from lazr.config.interfaces import ISectionSchema
>>> section_schema_1 = schema['section_1']
>>> verifyObject(ISectionSchema, section_schema_1)
True
Each SchemaSection has a name.
>>> print(section_schema_1.name)
section_1
A SchemaSection can return a 2-tuple of its category name and specific name
parts.
>>> for name in schema['section_3.app_b'].category_and_section_names:
... print(name)
section_3
app_b
The category name will be ``None`` if the SchemaSection's name does not
contain a category.
>>> for name in section_schema_1.category_and_section_names:
... print(name)
None
section_1
Optional sections have the optional attribute set to ``True``:
>>> section_schema_1.optional
False
>>> schema['section_3.app_a'].optional
True
A key can be verified to be in a section.
>>> 'key1' in section_schema_1
True
>>> 'nonkey' in section_schema_1
False
A key can be accessed directly using as a subscript of the SchemaSection. The
value is always a string.
>>> print(section_schema_1['key3'])
Launchpad rocks
>>> section_schema_1['key5']
''
An error is raised if a non-existent key is accessed.
>>> section_schema_1['not-exist']
Traceback (most recent call last):
...
KeyError: ...
In the conf file, ``[section_1]`` is a default section that defines keys and
values. The values specified in the section schema will be used as default
values if not overridden in the configuration. In the case of *key5*, the key
had no explicit value, so the value is an empty string.
>>> for key in sorted(section_schema_1):
... print(key, ':', section_schema_1[key])
key1 : foo
key2 : bar and baz
key3 : Launchpad rocks
key4 : Fc;k yeah!
key5 :
In the conf file ``[section_3.template]`` defines a common set of keys and
default values for ``[section_3.app_a]`` and ``[section_3.app_b]``. When a
section defines different keys and default values from the template, the new
data overlays the template data. This is the case for section
``[section_3.app_b]``.
>>> for section_schema in sorted(all_section_3, key=attrgetter('name')):
... print(section_schema.name)
... for key in sorted(section_schema):
... print(key, ':', section_schema[key])
section_3.app_a
key1 : 17
key2 : 3.1415
section_3.app_b
key1 : 17
key2 : changed
key3 : unique
ConfigSchema validation
=======================
The schema parser is self-validating. It checks that the character encoding
is ASCII, and that the data is not ambiguous or self-contradicting. Keys must
exist inside sections and section names may not be defined twice. Sections
may belong to only one category, and only letters, numbers, dots and dashes
may be present in section names.
.. For multilingual Python support reasons, we don't include testable examples
here. See ``test_config.py`` and ``lazr/config/interfaces.py`` for details.
IConfigLoader
=============
ConfigSchema implements the two methods in the IConfigLoader interface. A
Config is created by a schema using either the ``load()`` or ``loadFile()``
methods to return a Config instance.
>>> from lazr.config.interfaces import IConfigLoader
>>> verifyObject(IConfigLoader, schema)
True
The ``load()`` method accepts a filename.
>>> local_conf = resource_filename(
... 'lazr.config.tests.testdata', 'local.conf')
>>> config = schema.load(local_conf)
The ``loadFile()`` method accepts a file-like object and an optional filename
keyword argument. The filename argument must be passed if the file-like
object does not have a ``name`` attribute.
>>> try:
... from io import StringIO
... except ImportError:
... # Python 2
... from StringIO import StringIO
>>> bad_data = ("""
... [meta]
... metakey: unsupported
... [unknown-section]
... key1 = value1
... [section_1]
... keyn: unknown key
... key1: bad character in caf\xc3)
... [section_3.template]
... key1: schema suffixes are not permitted""")
>>> bad_config = schema.loadFile(
... StringIO(bad_data), 'bad conf')
.. The bad_config example will be used for validation tests.
Config
======
The config represents the local configuration of the process on a system. It
is validated with a schema. It extends the schema, or other conf files, to
define the specific differences from the extended files that are required to
run the local processes.
The object returned by ``load()`` provides both the ``IConfigData`` and
``IStackableConfig`` interfaces. ``IConfigData`` is for read-only access to
the configuration data. A process configuration is made up of a stack of
different ``IConfigData``. The ``IStackableConfig`` interface provides the
methods used to manipulate that stack of configuration overlays.
>>> from lazr.config.interfaces import IConfigData, IStackableConfig
>>> verifyObject(IConfigData, config)
True
>>> verifyObject(IStackableConfig, config)
True
Like the schema file, the conf file is made up of sections with keys. The
sections may belong to a category. Unlike the schema file, it does not have
template or optional sections. The ``[meta]`` section has the extends key
that declares that this conf extends ``shared.conf``.
>>> with open(local_conf, 'rt') as local_file:
... raw_conf = local_file.read()
>>> print(raw_conf)
[meta]
extends: shared.conf
# Localize a key for section_1.
[section_1]
key5: local value
# Accept the default values for the optional section-5.
[section-5]
The ``.master`` section allows admins to define configurations for an
arbitrary number of processes. If the schema defines ``.master`` sections,
then the conf file can contain sections that extend the ``.master`` section.
These are like categories with templates except that the section names
extending ``.master`` need not be named in the schema file.
>>> master_schema_conf = resource_filename(
... 'lazr.config.tests.testdata', 'master.conf')
>>> master_local_conf = resource_filename(
... 'lazr.config.tests.testdata', 'master-local.conf')
>>> master_schema = ConfigSchema(master_schema_conf)
>>> sections = master_schema.getByCategory('thing')
>>> for name in sorted(section.name for section in sections):
... print(name)
thing.master
>>> master_conf = master_schema.load(master_local_conf)
>>> sections = master_conf.getByCategory('thing')
>>> for name in sorted(section.name for section in sections):
... print(name)
thing.one
thing.two
>>> for name in sorted(section.foo for section in sections):
... print(name)
1
2
>>> print(master_conf.thing.one.name)
thing.one
The ``shared.conf`` file derives the keys and default values from the schema.
This config was loaded before ``local.conf`` because its sections and values
are required to be in place before ``local.conf`` applies its changes.
>>> shared_config = resource_filename(
... 'lazr.config.tests.testdata', 'shared.conf')
>>> with open(shared_config, 'rt') as shared_file:
... raw_conf = shared_file.read()
>>> print(raw_conf)
# The schema is defined by base.conf.
# Localize a key for section_1.
[section_1]
key2: sharing is fun
key5: shared value
The config that was loaded has ``name`` and ``filename`` attributes to
identify the configuration.
>>> print(config.name)
local.conf
>>> print('file:', config.filename)
file: ...lazr/config/tests/testdata/local.conf
The config can access the schema via the schema property.
>>> print(config.schema.name)
base.conf
>>> config.schema is schema
True
A config is made up of multiple Sections like the schema. They can be
iterated over in a loop as needed. This config inherited several sections
defined in schema. Note that the meta section is not present because it
pertains to the config system, not to the processes being configured.
>>> for section in sorted(config, key=attrgetter('name')):
... print(section.name)
section-2.app-b
section-5
section_1
section_3.app_b
section_33
You can check if a section name is in a config.
>>> 'section_1' in config
True
>>> 'bad-section' in config
False
Optional SchemaSections are not inherited by the config. A config file must
declare all optional sections. Including the section heading is enough to
inherit the section and its keys. The config file may localize the keys by
declaring them too. The ``local.conf`` file includes ``section-5``, but not
``section_3.app_a``.
>>> 'section_3.app_a' in config
False
>>> 'section_3.app_a' in config.schema
True
>>> config.schema['section_3.app_a'].optional
True
>>> 'section-5' in config
True
>>> 'section-5' in config.schema
True
>>> config.schema['section-5'].optional
True
A Section can be accessed using subscript notation. Accessing a section that
does not exist will raise a NoSectionError. NoSectionError is raised for a
undeclared optional sections too.
>>> section_1 = config['section_1']
>>> section_1.name in config
True
Config supports category access like Schema does. The list of categories are
returned by the ``category_names`` property.
>>> for name in sorted(config.category_names):
... print(name)
section-2
section_3
All the sections that belong to a category can be retrieved using the
``getByCategory()`` method.
>>> for section in config.getByCategory('section_3'):
... print(section_schema.name)
section_3.app_b
Passing a non-existent category_name to the method will raise a
NoCategoryError. As with schemas, you can pass a default argument to
``getByCategory()`` to avoid the exception.
>>> missing = object()
>>> config.getByCategory('non-section', missing) is missing
True
Section
=======
A Section behaves similar to a dictionary. It has keys and values. It
supports some specialize access methods and properties for working with the
values. Each Section has a name.
>>> from lazr.config.interfaces import ISection
>>> verifyObject(ISection, section_1)
True
>>> print(section_1.name)
section_1
Like SectionSchemas, sections can return a 2-tuple of their category name and
specific name parts. The category name will be ``None`` if the section's name
does not contain a category.
>>> for name in config['section_3.app_b'].category_and_section_names:
... print(name)
section_3
app_b
>>> for name in section_1.category_and_section_names:
... print(name)
None
section_1
The Section's type is the same type as the ``ConfigSchema.section_factory``.
>>> section_1
>>> config.schema.section_factory
A key can be verified to be in a Section.
>>> 'key1' in section_1
True
>>> 'nonkey' in section_1
False
A key can be accessed directly using as a subscript of the Section.
The value is always a string.
>>> print(section_1['key3'])
Launchpad rocks
>>> print(section_1['key5'])
local value
An error is raised if a non-existent key is accessed via a subscript.
>>> section_1['not-exist']
Traceback (most recent call last):
...
KeyError: ...
The Section keys can be iterated over. The section has all the keys from the
SectionSchema. The values came form the schema's default values, then the
values from ``shared.conf`` were applied, and lastly, the values from
``local.conf`` were applied. The schema provided the values of ``key1``,
``key3``, and ``key4``. ``shared.conf`` provided the value of ``key2``
. ``local.conf`` provided ``key5``. While ``shared.conf`` provided a
``key5``, ``local.conf`` takes precedence.
>>> for key in sorted(section_1):
... print(key, ':', section_1[key])
key1 : foo
key2 : sharing is fun
key3 : Launchpad rocks
key4 : Fc;k yeah!
key5 : local value
>>> section_1.schema['key5']
''
The schema provided mandatory sections and default values to the config. So
while the config file did not declare all the sections, they are present. In
the case of ``section_3.app_b``, its keys were defined in a template section.
>>> for key in sorted(config['section_3.app_b']):
... print(key, ':', config['section_3.app_b'][key])
key1 : 17
key2 : changed
key3 : unique
Sections attributes cannot be directly set to shadow config options. An
``AttributeError`` is raised when an attempt is made to mutate the config.
>>> config['section_3.app_b'].key1 = 'fail'
Traceback (most recent call last):
...
AttributeError: Config options cannot be set directly.
Nor can new attributes be added to a section.
>>> config['section_3.app_b'].no_such_attribute = 'fail'
Traceback (most recent call last):
...
AttributeError: Config options cannot be set directly.
Validating configs
==================
Config provides the ``validate()`` method to verify that the config is valid
according to the schema. The method returns ``True`` if the config is valid.
>>> config.validate()
True
When the config is not valid, a ConfigErrors is raised. The exception has an
``errors`` property that contains a list of all the errors in the config.
Config overlays
===============
A conf file may contain a meta section that is used by the config system. The
config data can access the config it extended using the ``extends`` property.
The object is just the config data; it does not have any config methods.
>>> print(config.extends.name)
shared.conf
>>> verifyObject(IConfigData, config.extends)
True
As Config supports inheritance through the ``extends`` key, each conf file
produces instance of ConfigData, called an *overlay*. ConfigData represents
the state of a config. The ``overlays`` property is a stack of ConfigData as
it was constructed from the schema's config to the last config file that was
loaded.
>>> for config_data in config.overlays:
... print(config_data.name)
local.conf
shared.conf
base.conf
>>> verifyObject(IConfigData, config.overlays[-1])
True
Conf files can use the ``extends`` key to specify that it extends a schema
without incurring a processing penalty by loading the schema twice in a row.
The schema can never be the second item in the overlays stack.
>>> single_config = schema.load(schema.filename)
>>> for config_data in single_config.overlays:
... print(config_data.name)
base.conf
>>> single_config.push(schema.filename, raw_schema.decode('utf-8'))
>>> for config_data in single_config.overlays:
... print(config_data.name)
base.conf
push()
======
Raw config data can be merged with the config to create a new overlay for
testing. The ``push()`` method accepts a string of config data. The data
must conform to the schema. The ``section_1`` sections's keys are updated
when the unparsed data is pushed onto the config. Note that indented,
unparsed data is passed to ``push()`` in this example; ``push()`` does not
require tests to dedent the test data.
::
>>> for key in sorted(config['section_1']):
... print(key, ':', config['section_1'][key])
key1 : foo
key2 : sharing is fun
key3 : Launchpad rocks
key4 : Fc;k yeah!
key5 : local value
>>> test_data = ("""
... [section_1]
... key1: test1
... key5:""")
>>> config.push('test config', test_data)
>>> for key in sorted(config['section_1']):
... print(key, ':', config['section_1'][key])
key1 : test1
key2 : sharing is fun
key3 : Launchpad rocks
key4 : Fc;k yeah!
key5 :
Besides updating section keys, optional sections can be enabled too. The
``section_3.app_a`` section is enabled with the default keys from the schema
in this example.
::
>>> config.schema['section_3.app_a'].optional
True
>>> 'section_3.app_a' in config
False
>>> app_a_data = "[section_3.app_a]"
>>> config.push('test app_a', app_a_data)
>>> 'section_3.app_a' in config
True
>>> for key in sorted(config['section_3.app_a']):
... print(key, ':', config['section_3.app_a'][key])
key1 : 17
key2 : 3.1415
>>> for key in sorted(config.schema['section_3.app_a']):
... print(key, ':', config.schema['section_3.app_a'][key])
key1 : 17
key2 : 3.1415
The config's name and overlays are updated by ``push()``.
>>> print(config.name)
test app_a
>>> print(config.filename)
test app_a
>>> for config_data in config.overlays:
... print(config_data.name)
test app_a
test config
local.conf
shared.conf
base.conf
The ``test app_a`` config did not declare an ``extends`` key in a ``meta``
section. Its ``extends`` property is ``None``, even though it implicitly
extends ``test config``. The ``extends`` property only provides access to
configs that are explicitly extended.
>>> print(config.extends.name)
test config
The config's sections are updated with ``section_3.app_a`` too.
>>> for section in sorted(config, key=attrgetter('name')):
... print(section.name)
section-2.app-b
section-5
section_1
section_3.app_a
section_3.app_b
section_33
A config file may state that it extends its schema (to clearly connect the
config to the schema). The schema can also be pushed to reset the values in
the config to the schema's default values.
>>> extender_conf_name = resource_filename(
... 'lazr.config.tests.testdata', 'extender.conf')
>>> extender_conf_data = ("""
... [meta]
... extends: base.conf""")
>>> config.push(extender_conf_name, extender_conf_data)
>>> for config_data in config.overlays:
... print(config_data.name)
extender.conf
base.conf
test app_a
test config
local.conf
shared.conf
base.conf
The ``section_1`` section was restored to the schema's default values.
>>> for key in sorted(config['section_1']):
... print(key, ':', config['section_1'][key])
key1 : foo
key2 : bar and baz
key3 : Launchpad rocks
key4 : Fc;k yeah!
key5 :
``push()`` can also be used to extend master sections.
::
>>> sections = sorted(master_conf.getByCategory('bar'),
... key=attrgetter('name'))
>>> for section in sections:
... print(section.name, section.baz)
bar.master badger
bar.soup cougar
>>> master_conf.push('override', """
... [bar.two]
... baz: dolphin
... """)
>>> sections = sorted(master_conf.getByCategory('bar'),
... key=attrgetter('name'))
>>> for section in sections:
... print(section.name, section.baz)
bar.soup cougar
bar.two dolphin
>>> master_conf.push('overlord', """
... [bar.three]
... baz: emu
... """)
>>> sections = sorted(master_conf.getByCategory('bar'),
... key=attrgetter('name'))
>>> for section in sections:
... print(section.name, section.baz)
bar.soup cougar
bar.three emu
bar.two dolphin
``push()`` works with master sections too.
::
>>> schema_file = StringIO("""\
... [thing.master]
... foo: 0
... bar: 0
... """)
>>> push_schema = ConfigSchema('schema.cfg', schema_file)
>>> config_file = StringIO("""\
... [thing.one]
... foo: 1
... """)
>>> push_config = push_schema.loadFile(config_file, 'config.cfg')
>>> print(push_config.thing.one.foo)
1
>>> print(push_config.thing.one.bar)
0
>>> push_config.push('test.cfg', """\
... [thing.one]
... bar: 2
... """)
>>> print(push_config.thing.one.foo)
1
>>> print(push_config.thing.one.bar)
2
pop()
=====
ConfigData can be removed from the stack of overlays using the ``pop()``
method. The methods returns the list of ConfigData that was removed -- a
slice from the specified ConfigData to the top of the stack.
::
>>> overlays = config.pop('test config')
>>> for config_data in overlays:
... print(config_data.name)
extender.conf
base.conf
test app_a
test config
>>> for config_data in config.overlays:
... print(config_data.name)
local.conf
shared.conf
base.conf
The config's state was restored to the ConfigData that is on top of the
overlay stack. Section ``section_3.app_a`` was removed completely. The keys
(``key1`` and ``key5``) for ``section_1`` were restored.
::
>>> for section in sorted(config, key=attrgetter('name')):
... print(section.name)
section-2.app-b
section-5
section_1
section_3.app_b
section_33
>>> for key in sorted(config['section_1']):
... print(key, ':', config['section_1'][key])
key1 : foo
key2 : sharing is fun
key3 : Launchpad rocks
key4 : Fc;k yeah!
key5 : local value
A Config must have at least one ConfigData in the overlays stack so that it
has data. The bottom ConfigData in the overlays was made from the schema's
required sections. It cannot be removed by the ``pop()`` method.
If all but the bottom ConfigData is popped from overlays, the extends
property returns None.
>>> overlays = config.pop('shared.conf')
>>> print(config.extends)
None
Attribute access to config data
===============================
Config provides attribute-based access to its members. So long as the
section, category, and key names conform to Python identifier naming rules,
they can be accessed as attributes. The Python code will not compile, or will
cause a runtime error if the object being accessed has a bad name.
Sections appear to be attributes of the config.
>>> config = schema.load(local_conf)
>>> config.section_1 is config['section_1']
True
Accessing an unknown section, or a section whose name is not a valid Python
identifier will raise an AttributeError.
>>> config.section-5
Traceback (most recent call last):
...
AttributeError: No section or category named section.
Categories may be accessed as attributes too. The ICategory interface
provides access to its sections as members.
>>> from lazr.config.interfaces import ICategory
>>> config_category = config.section_3
>>> verifyObject(ICategory, config_category)
True
>>> config_category.app_b is config['section_3.app_b']
True
Like a config, a category will raise an AttributeError if it does not have a
section that matches the identifier name.
>>> config_category.no_such_section
Traceback (most recent call last):
...
AttributeError: No section named no_such_section.
Section keys can be accessed directly as members.
>>> print(config.section_1.key2)
sharing is fun
>>> print(config.section_3.app_b.key2)
changed
Accessing a non-existent section key as an attribute will raise an
AttributeError.
>>> config.section_1.non_key
Traceback (most recent call last):
...
AttributeError: No section key named non_key.
Implicit data typing
====================
The ImplicitTypeSchema can create configs that support implicit datatypes.
The value of a Section key is automatically converted from ``str`` to the type
the value appears to be. Implicit typing does not add any validation support;
it adds type casting conveniences for the developer.
An ImplicitTypeSchema can be used to parse the same schema and conf files that
Schema uses.
>>> from lazr.config import ImplicitTypeSchema
>>> implicit_schema = ImplicitTypeSchema(base_conf)
>>> verifyObject(IConfigSchema, implicit_schema)
True
The config loaded by ImplicitTypeSchema is the same class with the same
sections as is made by Schema.
::
>>> implicit_config = implicit_schema.load(local_conf)
>>> implicit_config
>>> config
>>> sections = sorted(section.name for section in config)
>>> implicit_sections = sorted(
... section.name for section in implicit_config)
>>> implicit_sections == sections
True
>>> verifyObject(ISection, implicit_config['section_3.app_b'])
True
But the type of sections in the config support implicit typing.
>>> implicit_config['section_3.app_b']
ImplicitTypeSection, in contrast to Section, converts values that appear to be
integer or boolean into ints and bools.
::
>>> config['section_3.app_b']['key1']
'17'
>>> implicit_config['section_3.app_b']['key1']
17
>>> config['section-2.app-b']['key1']
'True'
>>> implicit_config['section-2.app-b']['key1']
True
The value is also converted when it is accessed as an attribute.
>>> implicit_config.section_3.app_b.key1
17
>>> implicit_config['section-2.app-b'].key1
True
ImplicitTypeSection uses a private method that employs heuristic rules to
convert strings into simple types. It may return a str, bool, or int. When
the argument is the word 'true' or 'false' (in any case), a bool is returned.
Values like 'yes', 'no', '0', and '1' are not converted to bool.
::
>>> convert = implicit_config['section_1']._convert
>>> convert('false')
False
>>> convert('TRUE')
True
>>> convert('tRue')
True
>>> print(convert('yes'))
yes
>>> convert('1')
1
>>> print(convert('True or False'))
True or False
When the argument is the word ``none``, ``None`` is returned. The token in
the config means the key has no value.
::
>>> print(convert('none'))
None
>>> print(convert('None'))
None
>>> print(convert('nonE'))
None
>>> print(convert('none today'))
none today
>>> print(convert('nonevident'))
nonevident
When the argument is an unbroken sequence of numbers, an int is returned. The
number may have a leading positive or negative. Octal and hex notation is not
supported.
::
>>> convert('0')
0
>>> convert('2001')
2001
>>> convert('-55')
-55
>>> convert('+404')
404
>>> convert('0100')
100
>>> print(convert('2001-01-01'))
2001-01-01
>>> print(convert('1000*60*5'))
1000*60*5
>>> print(convert('1000 * 60 * 5'))
1000 * 60 * 5
>>> print(convert('1,024'))
1,024
>>> print(convert('0.5'))
0.5
>>> print(convert('0x100'))
0x100
Multiline values are always strings, with white space (and line breaks)
removed from the beginning and end.
>>> print(convert("""multiline value 1
... multiline value 2"""))
multiline value 1
multiline value 2
Type conversion helpers
=======================
lazr.config provides a few helpers for doing explicit type conversion. These
functions have to be imported and called explicitly on the configuration
variable values.
Booleans
--------
There is a helper for turning various strings into the boolean values ``True``
and ``False``.
>>> from lazr.config import as_boolean
True values include (case-insensitively): true, yes, 1, on, enabled, and
enable.
>>> for value in ('true', 'yes', 'on', 'enable', 'enabled', '1'):
... print(value, '->', as_boolean(value))
... print(value.upper(), '->', as_boolean(value.upper()))
true -> True
TRUE -> True
yes -> True
YES -> True
on -> True
ON -> True
enable -> True
ENABLE -> True
enabled -> True
ENABLED -> True
1 -> True
1 -> True
False values include (case-insensitively): false, no, 0, off, disabled, and
disable.
>>> for value in ('false', 'no', 'off', 'disable', 'disabled', '0'):
... print(value, '->', as_boolean(value))
... print(value.upper(), '->', as_boolean(value.upper()))
false -> False
FALSE -> False
no -> False
NO -> False
off -> False
OFF -> False
disable -> False
DISABLE -> False
disabled -> False
DISABLED -> False
0 -> False
0 -> False
Anything else is a error.
>>> as_boolean('cheese')
Traceback (most recent call last):
...
ValueError: Invalid boolean value: cheese
Host and port
-------------
There is a helper for converting from a ``host:port`` string to a 2-tuple of
``(host, port)``.
>>> from lazr.config import as_host_port
>>> host, port = as_host_port('host:25')
>>> print(host, port)
host 25
The port string is optional, in which case, port 25 is the default (for
historical reasons).
>>> host, port = as_host_port('host')
>>> print(host, port)
host 25
The default port can be overridden.
>>> host, port = as_host_port('host', default_port=22)
>>> print(host, port)
host 22
The default port is ignored if it is given in the value.
>>> host, port = as_host_port('host:80', default_port=22)
>>> print(host, port)
host 80
The host name is also optional, as denoted by a leading colon. When omitted,
localhost is used.
>>> host, port = as_host_port(':80')
>>> print(host, port)
localhost 80
The default host name can be overridden though.
>>> host, port = as_host_port(':80', default_host='myhost')
>>> print(host, port)
myhost 80
The default host name is ignored if the value string contains it.
>>> host, port = as_host_port('yourhost:80', default_host='myhost')
>>> print(host, port)
yourhost 80
A ValueError occurs if the port number in the configuration value string is
not an integer.
>>> as_host_port(':foo')
Traceback (most recent call last):
...
ValueError: invalid literal for int...foo...
User and group
--------------
A helper is provided for turning a ``chown(1)``-style ``user:group``
specification into a 2-tuple of the user name and group name.
>>> from lazr.config import as_username_groupname
The value string must contain both a user name and group name, separated by a
colon, otherwise an exception is raised.
>>> as_username_groupname('foo')
Traceback (most recent call last):
...
ValueError: need more than 1 value to unpack
When both are given, the strings are returned unchanged or validated.
>>> user, group = as_username_groupname('person:group')
>>> print(user, group)
person group
Numeric values can be given, but they are not converted into their symbolic
names.
>>> uid, gid = as_username_groupname('25:26')
>>> print(uid, gid)
25 26
By default the current user and group names are returned.
>>> import grp, os, pwd
>>> user, group = as_username_groupname()
>>> user == pwd.getpwuid(os.getuid()).pw_name
True
>>> group == grp.getgrgid(os.getgid()).gr_name
True
Time intervals
--------------
This converter accepts a range of *time interval specifications*, and returns
a Python timedelta_.
>>> from lazr.config import as_timedelta
The function converts from an integer to the equivalent number of seconds.
>>> as_timedelta('45s')
datetime.timedelta(0, 45)
The function also accepts suffixes ``m`` for minutes...
>>> as_timedelta('3m')
datetime.timedelta(0, 180)
...``h`` for hours...
>>> as_timedelta('2h')
datetime.timedelta(0, 7200)
...and ``d`` for days...
>>> as_timedelta('4d')
datetime.timedelta(4)
...and ``w`` for weeks.
>>> as_timedelta('4w')
datetime.timedelta(28)
The function accepts a fractional number of seconds, indicating microseconds.
>>> as_timedelta('3.2s')
datetime.timedelta(0, 3, 200000)
It also accepts any combination thereof.
>>> as_timedelta('3m22.5s')
datetime.timedelta(0, 202, 500000)
>>> as_timedelta('4w2d9h3s')
datetime.timedelta(30, 32403)
But doesn't accept "weird" or duplicate combinations.
>>> as_timedelta('3s2s')
Traceback (most recent call last):
...
ValueError
>>> as_timedelta('2.9s4w')
Traceback (most recent call last):
...
ValueError
>>> as_timedelta('m')
Traceback (most recent call last):
...
ValueError
>>> as_timedelta('3m2')
Traceback (most recent call last):
...
ValueError
>>> as_timedelta('45')
Traceback (most recent call last):
...
ValueError
>>> as_timedelta('45wm')
Traceback (most recent call last):
...
ValueError
>>> as_timedelta('45z')
Traceback (most recent call last):
...
ValueError
Log levels
----------
It's convenient to be able to use symbolic log level names when using
``lazr.config`` to configure the Python logger.
>>> from lazr.config import as_log_level
Any symbolic log level value is valid to use, case insensitively.
>>> for value in ('critical', 'error', 'warning', 'info',
... 'debug', 'notset'):
... print(value, '->', as_log_level(value))
... print(value.upper(), '->', as_log_level(value.upper()))
critical -> 50
CRITICAL -> 50
error -> 40
ERROR -> 40
warning -> 30
WARNING -> 30
info -> 20
INFO -> 20
debug -> 10
DEBUG -> 10
notset -> 0
NOTSET -> 0
Non-log levels cannot be used here.
>>> as_log_level('cheese')
Traceback (most recent call last):
...
AttributeError: 'module' object has no attribute 'CHEESE'
Other Documents
===============
.. toctree::
:glob:
*
.. _timedelta: http://docs.python.org/3/library/datetime.html#timedelta-objects
lazr.config-2.1/lazr/config/docs/fixture.py 0000664 0001750 0001750 00000002340 12452627500 021261 0 ustar barry barry 0000000 0000000 # Copyright 2009-2015 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.smtptest
#
# lazr.smtptest is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.smtptest is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.smtptest. If not, see .
"""Doctest fixtures for running under nose."""
from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'globs',
]
def globs(globs):
"""Set up globals for doctests."""
# Enable future statements to make Python 2 act more like Python 3.
globs['absolute_import'] = absolute_import
globs['print_function'] = print_function
globs['unicode_literals'] = unicode_literals
# Provide a convenient way to clean things up at the end of the test.
return globs
lazr.config-2.1/lazr/__init__.py 0000664 0001750 0001750 00000001611 12452627440 017140 0 ustar barry barry 0000000 0000000 # Copyright 2008-2015 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.config.
#
# lazr.config is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.config is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.config. If not, see .
# This is a namespace package.
try:
import pkg_resources
pkg_resources.declare_namespace(__name__)
except ImportError:
import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__)
lazr.config-2.1/setup.py 0000775 0001750 0001750 00000004435 12452627550 015605 0 ustar barry barry 0000000 0000000 # Copyright 2008-2015 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.config.
#
# lazr.config is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.config is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.config. If not, see .
from setuptools import setup, find_packages
__version__ = open("lazr/config/version.txt").read().strip()
setup(
name='lazr.config',
version=__version__,
namespace_packages=['lazr'],
packages=find_packages(),
include_package_data=True,
zip_safe=False,
maintainer='LAZR Developers',
maintainer_email='lazr-developers@lists.launchpad.net',
description=('Create configuration schemas, and process and '
'validate configurations.'),
long_description="""
The LAZR config system is typically used to manage process configuration.
Process configuration is for saying how things change when we run systems on
different machines, or under different circumstances.
This system uses ini-like file format of section, keys, and values. The
config file supports inheritance to minimize duplication of information across
files. The format supports schema validation.
""",
license='LGPL v3',
install_requires=[
'nose',
'setuptools',
'zope.interface',
'lazr.delegates',
],
url='https://launchpad.net/lazr.config',
download_url='https://launchpad.net/lazr.config/+download',
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
"Operating System :: OS Independent",
'Programming Language :: Python',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
],
)
lazr.config-2.1/COPYING.txt 0000664 0001750 0001750 00000016725 12452627145 015746 0 ustar barry barry 0000000 0000000 GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
lazr.config-2.1/setup.cfg 0000664 0001750 0001750 00000000422 12452641133 015672 0 ustar barry barry 0000000 0000000 [nosetests]
verbosity = 3
with-coverage = 1
with-doctest = 1
doctest-extension = .rst
doctest-options = +ELLIPSIS,+NORMALIZE_WHITESPACE,+REPORT_NDIFF
doctest-fixtures = _fixture
cover-package = lazr.config
pdb = 1
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
lazr.config-2.1/PKG-INFO 0000664 0001750 0001750 00000002351 12452641133 015151 0 ustar barry barry 0000000 0000000 Metadata-Version: 1.1
Name: lazr.config
Version: 2.1
Summary: Create configuration schemas, and process and validate configurations.
Home-page: https://launchpad.net/lazr.config
Author: LAZR Developers
Author-email: lazr-developers@lists.launchpad.net
License: LGPL v3
Download-URL: https://launchpad.net/lazr.config/+download
Description:
The LAZR config system is typically used to manage process configuration.
Process configuration is for saying how things change when we run systems on
different machines, or under different circumstances.
This system uses ini-like file format of section, keys, and values. The
config file supports inheritance to minimize duplication of information across
files. The format supports schema validation.
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
lazr.config-2.1/tox.ini 0000664 0001750 0001750 00000000124 12452627145 015372 0 ustar barry barry 0000000 0000000 [tox]
envlist = py27,py32,py33,py34
[testenv]
commands = python setup.py nosetests
lazr.config-2.1/HACKING.rst 0000664 0001750 0001750 00000002667 12452627145 015673 0 ustar barry barry 0000000 0000000 ..
This file is part of lazr.config.
lazr.config is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, version 3 of the License.
lazr.config is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public License
along with lazr.config. If not, see .
======================
Hacking on lazr.config
======================
These are guidelines for hacking on the lazr.config project. But first,
please see the common hacking guidelines at:
http://dev.launchpad.net/Hacking
Getting help
------------
If you find bugs in this package, you can report them here:
https://launchpad.net/lazr.config
If you want to discuss this package, join the team and mailing list here:
https://launchpad.net/~lazr-developers
or send a message to:
lazr-developers@lists.launchpad.net
Running the tests
=================
The tests suite requires tox_ and nose_ and is compatible with both Python 2
and Python 3. To run the full test suite::
$ tox
.. _nose: https://nose.readthedocs.org/en/latest/
.. _tox: https://testrun.org/tox/latest/
lazr.config-2.1/lazr.config.egg-info/ 0000775 0001750 0001750 00000000000 12452641133 017761 5 ustar barry barry 0000000 0000000 lazr.config-2.1/lazr.config.egg-info/namespace_packages.txt 0000664 0001750 0001750 00000000005 12452641133 024307 0 ustar barry barry 0000000 0000000 lazr
lazr.config-2.1/lazr.config.egg-info/SOURCES.txt 0000664 0001750 0001750 00000002250 12452641133 021644 0 ustar barry barry 0000000 0000000 COPYING.txt
HACKING.rst
MANIFEST.in
README.rst
conf.py
setup.cfg
setup.py
tox.ini
lazr/__init__.py
lazr.config.egg-info/PKG-INFO
lazr.config.egg-info/SOURCES.txt
lazr.config.egg-info/dependency_links.txt
lazr.config.egg-info/namespace_packages.txt
lazr.config.egg-info/not-zip-safe
lazr.config.egg-info/requires.txt
lazr.config.egg-info/top_level.txt
lazr/config/__init__.py
lazr/config/_config.py
lazr/config/interfaces.py
lazr/config/version.txt
lazr/config/docs/NEWS.rst
lazr/config/docs/__init__.py
lazr/config/docs/fixture.py
lazr/config/docs/usage.rst
lazr/config/docs/usage_fixture.py
lazr/config/tests/__init__.py
lazr/config/tests/test_config.py
lazr/config/tests/testdata/__init__.py
lazr/config/tests/testdata/bad-invalid-name-chars.conf
lazr/config/tests/testdata/bad-invalid-name.conf
lazr/config/tests/testdata/bad-nonascii.conf
lazr/config/tests/testdata/bad-redefined-key.conf
lazr/config/tests/testdata/bad-redefined-section.conf
lazr/config/tests/testdata/bad-sectionless.conf
lazr/config/tests/testdata/base.conf
lazr/config/tests/testdata/local.conf
lazr/config/tests/testdata/master-local.conf
lazr/config/tests/testdata/master.conf
lazr/config/tests/testdata/shared.conf lazr.config-2.1/lazr.config.egg-info/dependency_links.txt 0000664 0001750 0001750 00000000001 12452641133 024027 0 ustar barry barry 0000000 0000000
lazr.config-2.1/lazr.config.egg-info/PKG-INFO 0000664 0001750 0001750 00000002351 12452641133 021057 0 ustar barry barry 0000000 0000000 Metadata-Version: 1.1
Name: lazr.config
Version: 2.1
Summary: Create configuration schemas, and process and validate configurations.
Home-page: https://launchpad.net/lazr.config
Author: LAZR Developers
Author-email: lazr-developers@lists.launchpad.net
License: LGPL v3
Download-URL: https://launchpad.net/lazr.config/+download
Description:
The LAZR config system is typically used to manage process configuration.
Process configuration is for saying how things change when we run systems on
different machines, or under different circumstances.
This system uses ini-like file format of section, keys, and values. The
config file supports inheritance to minimize duplication of information across
files. The format supports schema validation.
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
lazr.config-2.1/lazr.config.egg-info/top_level.txt 0000664 0001750 0001750 00000000005 12452641133 022506 0 ustar barry barry 0000000 0000000 lazr
lazr.config-2.1/lazr.config.egg-info/requires.txt 0000664 0001750 0001750 00000000056 12452641133 022362 0 ustar barry barry 0000000 0000000 nose
setuptools
zope.interface
lazr.delegates
lazr.config-2.1/lazr.config.egg-info/not-zip-safe 0000664 0001750 0001750 00000000001 12452641133 022207 0 ustar barry barry 0000000 0000000
lazr.config-2.1/conf.py 0000664 0001750 0001750 00000020413 12452627414 015360 0 ustar barry barry 0000000 0000000 # -*- coding: utf-8 -*-
#
# lazr.config documentation build configuration file, created by
# sphinx-quickstart on Mon Jan 7 10:37:37 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
from __future__ import print_function
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'README'
# General information about the project.
project = u'lazr.config'
copyright = u'2013-2015, LAZR developers'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = open('lazr/config/version.txt').read().strip()
# The full version, including alpha/beta/rc tags.
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build', 'eggs']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'lazrconfigdoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'lazrconfig.tex', u'lazr.config Documentation',
u'LAZR developers', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'lazrconfig', u'lazr.config Documentation',
[u'LAZR developers'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'lazrconfig', u'lazr.config Documentation',
u'LAZR developers', 'lazrconfig', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# Make upload to packages.python.org happy.
def index_html():
import errno
cwd = os.getcwd()
try:
try:
os.makedirs('build/sphinx/html')
except OSError as error:
if error.errno != errno.EEXIST:
raise
os.chdir('build/sphinx/html')
try:
os.symlink('README.html', 'index.html')
print('index.html -> README.html')
except OSError as error:
if error.errno != errno.EEXIST:
raise
finally:
os.chdir(cwd)
import atexit
atexit.register(index_html)
lazr.config-2.1/README.rst 0000664 0001750 0001750 00000000413 12452627145 015547 0 ustar barry barry 0000000 0000000 ======================
Welcome to lazr.config
======================
Contents:
.. toctree::
:maxdepth: 2
lazr/config/docs/usage
lazr/config/docs/NEWS
HACKING
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`