configglue-1.1.2/0000755000175000017500000000000012167605161014732 5ustar ricardoricardo00000000000000configglue-1.1.2/setup.py0000644000175000017500000000375712167065611016460 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2011 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from setuptools import ( find_packages, setup, ) import configglue install_requires = ['pyxdg'] setup(name='configglue', version=configglue.__version__, description="Glue to stick OptionParser and ConfigParser together", long_description=""" configglue is a library that glues together python's optparse.OptionParser and ConfigParser.ConfigParser, so that you don't have to repeat yourself when you want to export the same options to a configuration file and a commandline interface. """, classifiers=[ 'License :: OSI Approved :: BSD License', 'Topic :: Software Development :: Libraries :: Python Modules', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', ], author='John R. Lenton, Ricardo Kirkner', author_email='john.lenton@canonical.com, ricardo.kirkner@canonical.com', url='https://launchpad.net/configglue', license='BSD License', install_requires=install_requires, dependency_links=['http://www.freedesktop.org/wiki/Software/pyxdg'], packages=find_packages(), include_package_data=True, zip_safe=True, test_suite='configglue.tests', tests_require=['mock'], ) configglue-1.1.2/PKG-INFO0000644000175000017500000000205612167605161016032 0ustar ricardoricardo00000000000000Metadata-Version: 1.1 Name: configglue Version: 1.1.2 Summary: Glue to stick OptionParser and ConfigParser together Home-page: https://launchpad.net/configglue Author: John R. Lenton, Ricardo Kirkner Author-email: john.lenton@canonical.com, ricardo.kirkner@canonical.com License: BSD License Description: configglue is a library that glues together python's optparse.OptionParser and ConfigParser.ConfigParser, so that you don't have to repeat yourself when you want to export the same options to a configuration file and a commandline interface. Platform: UNKNOWN Classifier: License :: OSI Approved :: BSD License Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 configglue-1.1.2/LICENSE0000644000175000017500000000274012167600351015736 0ustar ricardoricardo00000000000000Copyright 2009--2013 Canonical Ltd. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY CANONICAL LTD. ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CANONICAL LTD. OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of Canonical Ltd. configglue-1.1.2/setup.cfg0000644000175000017500000000007312167605161016553 0ustar ricardoricardo00000000000000[egg_info] tag_svn_revision = 0 tag_build = tag_date = 0 configglue-1.1.2/configglue/0000755000175000017500000000000012167605161017054 5ustar ricardoricardo00000000000000configglue-1.1.2/configglue/glue.py0000644000175000017500000001023112167560044020357 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### import os import sys from optparse import OptionParser from collections import namedtuple from ._compat import NoSectionError, NoOptionError from .parser import SchemaConfigParser __all__ = [ 'configglue', 'schemaconfigglue', ] SchemaGlue = namedtuple("SchemaGlue", "schema_parser option_parser options args") def schemaconfigglue(parser, op=None, argv=None): """Glue an OptionParser with a SchemaConfigParser. The OptionParser is populated with options and defaults taken from the SchemaConfigParser. """ def long_name(option): if option.section.name == '__main__': return option.name return option.section.name + '_' + option.name def opt_name(option): return long_name(option).replace('-', '_') if op is None: op = OptionParser() if argv is None: argv = sys.argv[1:] schema = parser.schema for section in schema.sections(): if section.name == '__main__': og = op else: og = op.add_option_group(section.name) for option in section.options(): kwargs = {} if option.help: kwargs['help'] = option.help try: kwargs['default'] = parser.get(section.name, option.name) except (NoSectionError, NoOptionError): pass kwargs['action'] = option.action args = ['--' + long_name(option)] if option.short_name: # prepend the option's short name args.insert(0, '-' + option.short_name) og.add_option(*args, **kwargs) options, args = op.parse_args(argv) def set_value(section, option, value): # if value is not of the right type, cast it if not option.validate(value): kwargs = {} if option.require_parser: kwargs['parser'] = parser value = option.parse(value, **kwargs) parser.set(section.name, option.name, value) for section in schema.sections(): for option in section.options(): op_value = getattr(options, opt_name(option)) try: parser_value = parser.get(section.name, option.name) except (NoSectionError, NoOptionError): parser_value = None env_value = os.environ.get("CONFIGGLUE_{0}".format( long_name(option).upper())) # 1. op value != parser value # 2. op value == parser value != env value # 3. op value == parser value == env value or not env value # if option is fatal, op_value will be None, so skip this case too if op_value != parser_value and not option.fatal: set_value(section, option, op_value) elif env_value is not None and env_value != parser_value: set_value(section, option, env_value) return op, options, args def configglue(schema_class, configs, op=None, validate=False): """Parse configuration files using a provided schema. The standard workflow for configglue is to instantiate a schema class, feed it with some config files, and validate the parser state afterwards. This utility function executes this standard worfklow so you don't have to repeat yourself. """ scp = SchemaConfigParser(schema_class()) scp.read(configs) parser, opts, args = schemaconfigglue(scp, op=op) if validate or getattr(opts, 'validate', False): is_valid, reasons = scp.is_valid(report=True) if not is_valid: parser.error('\n'.join(reasons)) return SchemaGlue(scp, parser, opts, args) configglue-1.1.2/configglue/contrib/0000755000175000017500000000000012167605161020514 5ustar ricardoricardo00000000000000configglue-1.1.2/configglue/contrib/schema/0000755000175000017500000000000012167605161021754 5ustar ricardoricardo00000000000000configglue-1.1.2/configglue/contrib/schema/django_openid_auth.py0000644000175000017500000000426012167556013026152 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from configglue.schema import ( BoolOption, DictOption, ListOption, Schema, Section, StringOption, ) class DjangoOpenIdAuthSchema(Schema): """Configglue schema for django-openid-auth.""" __version__ = '0.5' class openid(Section): openid_use_as_admin_login = BoolOption( default=False) openid_create_users = BoolOption( default=False) openid_update_details_from_sreg = BoolOption( default=False) openid_physical_multifactor_required = BoolOption( default=False) openid_strict_usernames = BoolOption( default=False) openid_sreg_required_fields = ListOption( item=StringOption()) openid_sreg_extra_fields = ListOption( item=StringOption()) openid_follow_renames = BoolOption( default=False) openid_launchpad_teams_mapping_auto = BoolOption( default=False) openid_launchpad_teams_mapping_auto_blacklist = ListOption( item=StringOption()) openid_launchpad_teams_mapping = DictOption() openid_launchpad_staff_teams = ListOption( item=StringOption()) openid_launchpad_teams_required = ListOption( item=StringOption()) openid_disallow_inames = BoolOption( default=False) allowed_external_openid_redirect_domains = ListOption( item=StringOption()) openid_trust_root = StringOption() openid_sso_server_url = StringOption( null=True) openid_email_whitelist_regexp_list = ListOption( item=StringOption()) configglue-1.1.2/configglue/contrib/schema/devserver.py0000644000175000017500000000547312167556006024347 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from configglue.schema import ( BoolOption, IntOption, ListOption, Section, Schema, StringOption, ) class DevServerSchema(Schema): """Configglue schema for django-devserver.""" __version__ = '0.3.1' class devserver(Section): devserver_args = ListOption( item=StringOption(), default=[], help='Additional command line arguments to pass to the runserver command (as defaults).') devserver_default_addr = StringOption( default='127.0.0.1', help='The default address to bind to.') devserver_default_port = StringOption( default='8000', help='The default port to bind to.') devserver_wsgi_middleware = ListOption( item=StringOption(), default=[], help='A list of additional WSGI middleware to apply to the runserver command.') devserver_modules = ListOption( item=StringOption(), default=[ 'devserver.modules.sql.SQLRealTimeModule', ], help='List of devserver modules to enable') devserver_ignored_prefixes = ListOption( item=StringOption(), default=['/media', '/uploads'], help='List of prefixes to supress and skip process on. By default, ' 'ADMIN_MEDIA_PREFIX, MEDIA_URL and STATIC_URL ' '(for Django >= 1.3) will be ignored (assuming MEDIA_URL and ' 'STATIC_URL is relative).') devserver_truncate_sql = BoolOption( default=True, help='Truncate SQL queries output by SQLRealTimeModule.') devserver_truncate_aggregates = BoolOption( default=False) devserver_active = BoolOption( default=False) devserver_ajax_content_length = IntOption( default=300, help='Maximum response length to dump.') devserver_ajax_pretty_print = BoolOption( default=False) devserver_sql_min_duration = IntOption( default=None, help='Minimum time a query must execute to be shown, value is in ms.') devserver_auto_profile = BoolOption( default=False, help='Automatically profile all view functions.') configglue-1.1.2/configglue/contrib/schema/preflight.py0000644000175000017500000000162212167556026024320 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from configglue.schema import Section, Schema, StringOption class PreflightSchema(Schema): """Configglue schema for django-preflight.""" __version__ = '0.1' class preflight(Section): preflight_base_template = StringOption( default='index.1col.html') preflight_table_class = StringOption( default='listing') configglue-1.1.2/configglue/contrib/schema/__init__.py0000644000175000017500000000206212167556020024064 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from .devserver import DevServerSchema from .django_jenkins import DjangoJenkinsSchema from .nexus import NexusSchema from .django_openid_auth import DjangoOpenIdAuthSchema from .preflight import PreflightSchema from .saml2idp import Saml2IdpSchema from .raven import RavenSchema from .pystatsd import PyStatsdSchema __all__ = [ 'DevServerSchema', 'DjangoJenkinsSchema', 'NexusSchema', 'DjangoOpenIdAuthSchema', 'PreflightSchema', 'Saml2IdpSchema', 'RavenSchema', 'PyStatsdSchema', ] configglue-1.1.2/configglue/contrib/schema/django_jenkins.py0000644000175000017500000000306612167556011025315 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from configglue.schema import ( ListOption, Schema, Section, StringOption, TupleOption, ) class DjangoJenkinsSchema(Schema): """Configglue schema for django-jenkins.""" __version__ = '0.12.1' class django_jenkins(Section): project_apps = ListOption( item=StringOption(), default=[], help='List of of django apps for Jenkins to run.') jenkins_tasks = TupleOption( default=( 'django_jenkins.tasks.run_pylint', 'django_jenkins.tasks.with_coverage', 'django_jenkins.tasks.django_tests', ), help='List of Jenkins tasks executed by ./manage.py jenkins ' 'command.') jenkins_test_runner = StringOption( default='', help='The name of the class to use for starting the test suite for ' 'jenkins and jtest commands. Class should be inherited from ' 'django_jenkins.runner.CITestSuiteRunner.') configglue-1.1.2/configglue/contrib/schema/raven.py0000644000175000017500000000611512167556032023446 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from configglue.schema import ( BoolOption, IntOption, ListOption, Schema, Section, StringOption, ) class RavenSchema(Schema): """Configglue schema for raven.""" __version__ = '1.6.1' class raven(Section): sentry_servers = ListOption() sentry_include_paths = ListOption( item=StringOption()) sentry_exclude_paths = ListOption( item=StringOption(), help='Ignore module prefixes when attempting to discover which ' 'function an error comes from.') sentry_timeout = IntOption( default=5, help='Timeout value for sending messages to remote.') sentry_name = StringOption( null=True, help='This will override the server_name value for this installation.') sentry_auto_log_stacks = BoolOption( default=False, help='Should raven automatically log frame stacks (including ' 'locals) all calls as it would for exceptions.') sentry_key = StringOption( null=True) sentry_max_length_string = IntOption( default=200, help='The maximum characters of a string that should be stored.') sentry_max_length_list = IntOption( default=50, help='The maximum number of items a list-like container should store.') sentry_site = StringOption( null=True, help='An optional, arbitrary string to identify this client ' 'installation.') sentry_public_key = StringOption( null=True, help='Public key of the project member which will authenticate as ' 'the client.') sentry_private_key = StringOption( null=True, help='Private key of the project member which will authenticate as ' 'the client.') sentry_project = IntOption( default=1, help='Sentry project ID. The default value for installations is 1.') sentry_processors = ListOption( item=StringOption(), default=[ 'raven.processors.SanitizePasswordsProcessor', ], help='List of processors to apply to events before sending them ' 'to the Sentry server.') sentry_dsn = StringOption( help='A sentry compatible DSN.') sentry_client = StringOption( default='raven.contrib.django.DjangoClient') sentry_debug = BoolOption( default=False) configglue-1.1.2/configglue/contrib/schema/saml2idp.py0000644000175000017500000000325112167556033024045 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from configglue.schema import ( BoolOption, ListOption, Section, Schema, StringOption, ) class Saml2IdpSchema(Schema): """Configglue schema for saml2idp.""" __version__ = '0.14' class saml2(Section): saml2idp_autosubmit = BoolOption( default=True) saml2idp_issuer = StringOption( default='http://127.0.0.1:8000') saml2idp_certificate_file = StringOption( default='keys/certificate.pem') saml2idp_private_key_file = StringOption( default='keys/private-key.pem') saml2idp_signing = BoolOption( default=True) saml2idp_valid_acs = ListOption( item=StringOption(), default=[ 'https://login.salesforce.com', ], help="List of ACS URLs accepted by /+saml login") saml2idp_processor_classes = ListOption( item=StringOption(), default=[ 'saml2idp.salesforce.Processor', 'saml2idp.google_apps.Processor', ], help="List of SAML 2.0 AuthnRequest processors") configglue-1.1.2/configglue/contrib/schema/pystatsd.py0000644000175000017500000000155712167556027024217 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from configglue.schema import IntOption, Schema, Section, StringOption class PyStatsdSchema(Schema): """Configglue schema for pystatsd.""" __version__ = '0.1.6' class statsd(Section): statsd_host = StringOption( default='localhost') statsd_port = IntOption( default=8125) configglue-1.1.2/configglue/contrib/schema/nexus.py0000644000175000017500000000160512167556023023474 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from configglue.schema import BoolOption, Schema, Section, StringOption class NexusSchema(Schema): """Configglue schema for nexus.""" __version__ = '0.2.3' class nexus(Section): nexus_media_prefix = StringOption( default='/nexus/media/') nexus_use_django_media_url = BoolOption( default=False) configglue-1.1.2/configglue/contrib/__init__.py0000644000175000017500000000112312167555752022634 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### # import all schemas from .schema import * configglue-1.1.2/configglue/__init__.py0000644000175000017500000000107612167604707021176 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### __version__ = '1.1.2' configglue-1.1.2/configglue/schema.py0000644000175000017500000005637312167560104020701 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from __future__ import unicode_literals import json from copy import deepcopy from inspect import getmembers from ._compat import text_type, string_types from ._compat import NoSectionError, NoOptionError __all__ = [ 'BoolOption', 'Option', 'Section', 'DictOption', 'IntOption', 'ListOption', 'Schema', 'StringOption', 'TupleOption', 'merge', ] NO_DEFAULT = object() def get_config_objects(obj): """Return the list of Section- and Option-derived objects.""" objects = [] for name, obj in getmembers(obj): if isinstance(obj, (Section, Option)): objects.append((name, obj)) elif type(obj) == type and issubclass(obj, Section): instance = obj() for key, value in get_config_objects(obj): setattr(instance, key, value) objects.append((name, instance)) return objects def merge(*schemas): # import here to avoid circular imports from .parser import SchemaValidationError # define result schema class MergedSchema(Schema): pass # for each schema for schema in schemas: instance = schema() # iterate over the schema sections for section in instance.sections(): # create the appropriate section object section_name = section.name if section_name == '__main__': # section is special __main__ section sect = MergedSchema elif not hasattr(MergedSchema, section_name): # section is not present in schema, just copy it over # completely setattr(MergedSchema, section_name, section) continue else: # section is present in schema, do the merge sect = getattr(MergedSchema, section_name) # do the merge # iterate over the section options for option in section.options(): opt = getattr(sect, option.name, None) if opt is not None: if option != opt: raise SchemaValidationError("Conflicting option " "'%s.%s' while merging schemas." % ( section_name, option.name)) else: setattr(sect, option.name, option) return MergedSchema class Schema(object): """A complete description of a system configuration. To define your own configuration schema you should: 1- Inherit from Schema 2- Add Options and Sections as class attributes. With that your whole configuration schema is defined, and you can now load configuration files. Options that don't go in a Section will belong in the '__main__' section of the configuration files. One Option comes already defined in Schema, 'includes' in the '__main__' section, that allows configuration files to include other configuration files. """ def __init__(self): self.includes = ListOption(item=StringOption()) self._sections = {} # add section and options to the schema for name, item in get_config_objects(self.__class__): self._add_item(name, item) def _add_item(self, name, item): """Add a top-level item to the schema.""" item.name = name if isinstance(item, Section): self._add_section(name, item) elif isinstance(item, Option): self._add_option(name, item) # override class attributes with instance attributes to correctly # handle schema inheritance setattr(self, name, deepcopy(item)) def _add_section(self, name, section): """Add a top-level section to the schema.""" self._sections[name] = section for opt_name, opt in get_config_objects(section): opt.name = opt_name opt.section = section def _add_option(self, name, option): """Add a top-level option to the schema.""" section = self._sections.setdefault('__main__', Section(name='__main__')) option.section = section setattr(section, name, option) def __eq__(self, other): return ( type(self) == type(other) and self._sections == other._sections and self.includes == other.includes) def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return id(self) def is_valid(self): """Return whether the schema has a valid structure.""" explicit_default_section = isinstance(getattr(self, '__main__', None), Section) is_valid = not explicit_default_section return is_valid def has_section(self, name): """Return True if a Section with the given name is available""" return name in self._sections.keys() def section(self, name): """Return a Section by name""" section = self._sections.get(name) if section is None: raise NoSectionError(name) return section def sections(self): """Returns the list of available Sections""" return self._sections.values() def options(self, section=None): """Return all the Options within a given section. If section is omitted, returns all the options in the configuration file, flattening out any sections. To get options from the default section, specify section='__main__' """ if isinstance(section, string_types): section = self.section(section) if section is None: options = [] for s in self.sections(): options += self.options(s) elif section.name == '__main__': class_config_objects = get_config_objects(self.__class__) options = [getattr(self, att) for att, _ in class_config_objects if isinstance(getattr(self, att), Option)] else: options = section.options() return options class Section(object): """A group of options. This class is just a bag you can dump Options in. After instantiating the Schema, each Section will know its own name. """ def __init__(self, name=''): self.name = name def __eq__(self, other): return ( type(self) == type(other) and self.name == other.name and self.options() == other.options()) def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return id(self) def __repr__(self): if self.name: name = " %s" % self.name else: name = '' value = "<{0}{1}>".format(self.__class__.__name__, name) return value def has_option(self, name): """Return True if a Option with the given name is available""" opt = getattr(self, name, None) return isinstance(opt, Option) def option(self, name): """Return a Option by name""" opt = getattr(self, name, None) if opt is None: raise NoOptionError(name, self.name) return opt def options(self): """Return a list of all available Options within this section""" return [getattr(self, att) for att in vars(self) if isinstance(getattr(self, att), Option)] class Option(object): """Base class for Config Options. Options are never bound to a particular conguration file, and simply describe one particular available option. They also know how to parse() the content of a config file in to the right type of object. If self.raw == True, then variable interpolation will not be carried out for this config option. If self.require_parser == True, then the parse() method will have a second argument, parser, that should receive the whole SchemaConfigParser to do the parsing. This is needed for config options that need to look at other parts of the config file to be able to carry out their parsing, like DictOption. If self.fatal == True, SchemaConfigParser's parse_all will raise an exception if no value for this option is provided in the configuration file. Otherwise, the self.default value will be used if the option is omitted. In runtime, after instantiating the Schema, each Option will also know its own name and to which section it belongs. """ require_parser = False def __init__(self, name='', raw=False, default=NO_DEFAULT, fatal=False, help='', section=None, action='store', short_name=''): self.name = name self.short_name = short_name self.raw = raw self.fatal = fatal if default is NO_DEFAULT: default = self._get_default() self.default = default self.help = help self.section = section self.action = action def __eq__(self, other): try: equal = ( type(self) == type(other) and self.name == other.name and self.short_name == other.short_name and self.raw == other.raw and self.fatal == other.fatal and self.default == other.default and self.help == other.help and self.action == other.action) if self.section is not None and other.section is not None: # sections might be different while the options are the # same; the section names though should be the same equal &= self.section.name == other.section.name else: equal &= (self.section is None and other.section is None) except AttributeError: equal = False return equal def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return id(self) def __repr__(self): extra = ' raw' if self.raw else '' extra += ' fatal' if self.fatal else '' section = self.section.name if self.section is not None else None if section is not None: name = " {0}.{1}".format(section, self.name) elif self.name: name = " {0}".format(self.name) else: name = '' value = "<{0}{1}{2}>".format(self.__class__.__name__, name, extra) return value def _get_default(self): return None def parse(self, value, raw=False): """Parse the given value.""" raise NotImplementedError() def validate(self, value): raise NotImplementedError() def to_string(self, value): """Return a string representation of the value.""" return text_type(value) class BoolOption(Option): """A Option that is parsed into a bool""" def _get_default(self): return False def parse(self, value, raw=False): """Parse the given value. If *raw* is *True*, return the value unparsed. """ if raw: return value if value.lower() in ['y', '1', 'yes', 'on', 'true']: return True elif value.lower() in ['n', '0', 'no', 'off', 'false']: return False else: raise ValueError("Unable to determine boolosity of %r" % value) def validate(self, value): return isinstance(value, bool) class IntOption(Option): """A Option that is parsed into an int""" def _get_default(self): return 0 def parse(self, value, raw=False): """Parse the given value. If *raw* is *True*, return the value unparsed. """ if raw: return value return int(value) def validate(self, value): return isinstance(value, int) class ListOption(Option): """A Option that is parsed into a list of objects All items in the list need to be of the same type. The 'item' constructor argument determines the type of the list items. item should be another child of Option. self.require_parser will be True if the item provided in turn has require_parser == True. if remove_duplicates == True, duplicate elements in the lines will be removed. Only the first occurrence of any item will be kept, otherwise the general order of the list will be preserved. """ def __init__(self, name='', item=None, raw=False, default=NO_DEFAULT, fatal=False, help='', action='store', remove_duplicates=False, short_name='', parse_json=True): super(ListOption, self).__init__(name=name, raw=raw, default=default, fatal=fatal, help=help, action=action, short_name=short_name) if item is None: item = StringOption() self.item = item self.require_parser = item.require_parser self.raw = raw or item.raw self.remove_duplicates = remove_duplicates self.parse_json = parse_json def __eq__(self, other): equal = super(ListOption, self).__eq__(other) if equal: # we can be sure both objects are of the same type by now equal &= (self.item == other.item and self.require_parser == other.require_parser and self.raw == other.raw and self.remove_duplicates == other.remove_duplicates) return equal def __hash__(self): return id(self) def _get_default(self): return [] def parse(self, value, parser=None, raw=False): """Parse the given value. A *parser* object is used to parse individual list items. If *raw* is *True*, return the value unparsed. """ def _parse_item(value): if self.require_parser: value = self.item.parse(value, parser=parser, raw=raw) else: value = self.item.parse(value, raw=raw) return value is_json = self.parse_json if is_json: try: parsed = json.loads(value) is_json = isinstance(parsed, list) except: is_json = False if not is_json: parsed = [_parse_item(x) for x in value.split('\n') if len(x)] if self.remove_duplicates: filtered_items = [] for item in parsed: if not item in filtered_items: filtered_items.append(item) parsed = filtered_items return parsed def validate(self, value): return isinstance(value, list) def to_string(self, value): if self.parse_json: return json.dumps(value) else: return super(ListOption, self).to_string(value) class StringOption(Option): """A Option that is parsed into a string. If null==True, a value of 'None' will be parsed in to None instead of just leaving it as the string 'None'. """ def __init__(self, name='', raw=False, default=NO_DEFAULT, fatal=False, null=False, help='', action='store', short_name=''): self.null = null super(StringOption, self).__init__(name=name, raw=raw, default=default, fatal=fatal, help=help, action=action, short_name=short_name) def __eq__(self, other): equal = super(StringOption, self).__eq__(other) if equal: # we can be sure both objects are of the same type by now equal &= self.null == other.null return equal def __hash__(self): return id(self) def _get_default(self): return '' if not self.null else None def parse(self, value, raw=False): """Parse the given value. If *raw* is *True*, return the value unparsed. """ if raw: result = value elif self.null: result = None if value in (None, 'None') else value elif isinstance(value, string_types): result = value else: result = repr(value) return result def to_string(self, value): if value is None and self.null: return 'None' return value def validate(self, value): return (self.null and value is None) or isinstance(value, string_types) class TupleOption(Option): """A Option that is parsed into a fixed-size tuple of strings. The number of items in the tuple should be specified with the 'length' constructor argument. """ def __init__(self, name='', length=0, raw=False, default=NO_DEFAULT, fatal=False, help='', action='store', short_name=''): super(TupleOption, self).__init__(name=name, raw=raw, default=default, fatal=fatal, help=help, action=action, short_name=short_name) self.length = length def __eq__(self, other): equal = super(TupleOption, self).__eq__(other) if equal: # we can be sure both objects are of the same type by now equal &= self.length == other.length return equal def __hash__(self): return id(self) def _get_default(self): return () def parse(self, value, raw=False): """Parse the given value. If *raw* is *True*, return the value unparsed. """ parts = [part.strip() for part in value.split(',')] if parts == ['()']: result = () elif self.length: # length is not 0, so length validation if len(parts) == self.length: result = tuple(parts) else: raise ValueError( "Tuples need to be %d items long" % self.length) else: result = tuple(parts) # length is 0, so no length validation return result def validate(self, value): return isinstance(value, tuple) class DictOption(Option): """A Option that is parsed into a dictionary. In the configuration file you'll need to specify the name of a section, and all that section's items will be parsed as a dictionary. The available keys for the dict are specified with the 'spec' constructor argument, that should be in turn a dictionary. spec's keys are the available keys for the config file, and spec's values should be Options that will be used to parse the values in the config file. """ require_parser = True def __init__(self, name='', spec=None, strict=False, raw=False, default=NO_DEFAULT, fatal=False, help='', action='store', item=None, short_name='', parse_json=True): if spec is None: spec = {} if item is None: item = StringOption() self.spec = spec self.strict = strict self.item = item self.parse_json = parse_json super(DictOption, self).__init__(name=name, raw=raw, default=default, fatal=fatal, help=help, action=action, short_name=short_name) def __eq__(self, other): equal = super(DictOption, self).__eq__(other) if equal: # we can be sure both objects are of the same type by now equal &= (self.spec == other.spec and self.strict == other.strict and self.item == other.item) return equal def __hash__(self): return id(self) def _get_default(self): default = {} for key, value in self.spec.items(): default[key] = value.default return default def parse(self, value, parser, raw=False): """Parse the given value. A *parser* object is used to parse individual dict items. If *raw* is *True*, return the value unparsed. """ is_json = self.parse_json if is_json: try: parsed = json.loads(value) is_json = isinstance(parsed, dict) except: is_json = False if not is_json: # process extra sections sections = value.split() parser.extra_sections.update(set(sections)) for name in sections: nested = self.get_extra_sections(name, parser) parser.extra_sections.update(set(nested)) parsed = dict(parser.items(value)) result = {} # parse config items according to spec for key, value in parsed.items(): if self.strict and not key in self.spec: raise ValueError("Invalid key %s in section %s" % ( key, value)) option = self.spec.get(key, None) if option is None: # option not part of spec, but we are in non-strict mode # parse it using the default item parser option = self.item if not option.validate(value): # parse option kwargs = {} if option.require_parser: kwargs['parser'] = parser if not raw: value = option.parse(value, **kwargs) result[key] = value # fill in missing items with default values for key in self.spec: if not key in parsed: option = self.spec[key] if option.fatal: raise ValueError("No option '%s' in section '%s'" % (key, value)) else: if not raw: value = option.default else: value = text_type(option.default) result[key] = value return result def validate(self, value): return isinstance(value, dict) def to_string(self, value): if self.parse_json: return json.dumps(value) else: return super(DictOption, self).to_string(value) def get_extra_sections(self, section, parser): """Return the list of implicit sections. Implicit sections are sections defined in the configuration file that are not defined in the schema, but used as helper sections for defining a dictionary. """ sections = [] for option in parser.options(section): option_obj = self.spec.get(option, self.item) is_dict_item = isinstance(option_obj, DictOption) is_dict_lines_item = (hasattr(option_obj, 'item') and isinstance(option_obj.item, DictOption)) if not (is_dict_item or is_dict_lines_item): continue if is_dict_item: base = option_obj else: base = option_obj.item value = parser.get(section, option, parse=False) names = value.split() sections.extend(names) # recurse for name in names: extra = base.get_extra_sections(name, parser) sections.extend(extra) return sections configglue-1.1.2/configglue/inischema/0000755000175000017500000000000012167605161021014 5ustar ricardoricardo00000000000000configglue-1.1.2/configglue/inischema/typed.py0000644000175000017500000000662312167556064022531 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### """ TypedConfigParser lives here """ from __future__ import absolute_import, unicode_literals import os from configglue._compat import text_type from . import parsers from .attributed import AttributedConfigParser class TypedConfigParser(AttributedConfigParser): """Building on AttributedConfigParser, handle the idea of having a configuration file that knows what type its options are. """ def __init__(self, *args, **kwargs): super(TypedConfigParser, self).__init__(*args, **kwargs) self.parsers = {'bool': parsers.bool_parser, 'complex': complex, 'float': float, 'int': int, 'lines': parsers.lines, 'unicode': text_type, 'getenv': os.getenv, None: lambda x: x} def add_parser(self, name, parser, clobber=False): """Add a custom parser @param name: the name with which you can ask for this parser in the configuration file @param parser: the parser itself @param clobber: whether to overwite an existing parser """ if name not in self.parsers or clobber: self.parsers[name] = parser else: raise ValueError('A parser by that name already exists') def add_parsers(self, *args): """Add multiple custom parsers @param args: any number of (name, parser, [clobber]) tuples """ for arg in args: self.add_parser(*arg) def parse(self, section, option): """Parse a single option in a single section. This actually consumes the 'parser', 'parser_args' and 'default' attributes. @param section: the section within which to look for the option @param option: the 'base' option to parse """ super(TypedConfigParser, self).parse(section, option) value = self.get(section, option) if 'default.parser' in value.attrs: parser = self.parsers[value.attrs.pop('default.parser')] value.attrs['default'] = parser(value.attrs['default']) if value.is_empty: if 'default' in value.attrs: value.value = value.attrs['default'] else: value.value = None if 'parser' in value.attrs: args = value.attrs.pop('parser.args', ()) if args != (): args_parser = value.attrs.pop('parser.args.parser', 'lines') args = self.parsers[args_parser](args) # leave the parser hanging around for if you need it later value.parser = self.parsers[value.attrs.pop('parser')] value.value = value.parser(value.value, *args) else: value.parser = self.parsers[None] # tadaa! self.set(section, option, value) configglue-1.1.2/configglue/inischema/glue.py0000644000175000017500000000643412167556055022340 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### """configglue lives here """ from __future__ import absolute_import from collections import namedtuple from configglue._compat import builtins from configglue.inischema import parsers from configglue.inischema.attributed import AttributedConfigParser from configglue.glue import schemaconfigglue from configglue.parser import SchemaConfigParser from configglue.schema import ( BoolOption, Section, IntOption, ListOption, Schema, StringOption, ) __all__ = [ 'configglue', 'ini2schema', ] IniGlue = namedtuple("IniGlue", " option_parser options args") def ini2schema(fd, p=None): """ Turn a fd that refers to a INI-style schema definition into a SchemaConfigParser object @param fd: file-like object to read the schema from @param p: a parser to use. If not set, uses AttributedConfigParser """ if p is None: p = AttributedConfigParser() p.readfp(fd) p.parse_all() parser2option = {'unicode': StringOption, 'int': IntOption, 'bool': BoolOption, 'lines': ListOption} class MySchema(Schema): pass for section_name in p.sections(): if section_name == '__main__': section = MySchema else: section = Section(name=section_name) setattr(MySchema, section_name, section) for option_name in p.options(section_name): option = p.get(section_name, option_name) parser = option.attrs.pop('parser', 'unicode') parser_args = option.attrs.pop('parser.args', '').split() parser_fun = getattr(parsers, parser, None) if parser_fun is None: parser_fun = getattr(builtins, parser, None) if parser_fun is None: parser_fun = lambda x: x attrs = {'name': option_name} option_help = option.attrs.pop('help', None) if option_help is not None: attrs['help'] = option_help if not option.is_empty: attrs['default'] = parser_fun(option.value, *parser_args) option_action = option.attrs.pop('action', None) if option_action is not None: attrs['action'] = option_action klass = parser2option.get(parser, StringOption) if parser == 'lines': instance = klass(item=StringOption(), **attrs) else: instance = klass(**attrs) setattr(section, option_name, instance) return SchemaConfigParser(MySchema()) def configglue(fileobj, *filenames, **kwargs): args = kwargs.pop('args', None) parser, opts, args = schemaconfigglue(ini2schema(fileobj), argv=args) return IniGlue(parser, opts, args) configglue-1.1.2/configglue/inischema/__init__.py0000644000175000017500000000357412167556057023147 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### """configglue -- glue for your apps' configuration Three things: AttributedConfigParser: a ConfigParser that gives you attributed options. So a config file with [foo] bar = Hello World bar.level = newbie will have one option under the 'foo' section, and that option will have a value ('Hello World') and an attribute 'level', with value 'newbie'. TypedConfigParser: an AttributedConfigParser that uses the 'parser' attribtue to parse the value. So [foo] bar = 7 bar.parser = int will have a 'foo' section with a 'bar' option which value is int('7'). configglue: A function that creates an TypedConfigParser and uses it to build an optparse.OptionParser instance. So you can have a config file with [foo] bar.default = 7 bar.help = The bar number [%(default)s] bar.metavar = BAR bar.parser = int and if you load it with configglue, you get something like $ python sample.py --help Usage: sample.py [options] Options: -h, --help show this help message and exit blah: --foo-bar=BAR The bar number [7] """ from __future__ import absolute_import from .typed import TypedConfigParser from .attributed import AttributedConfigParser from .glue import configglue __all__ = [ 'TypedConfigParser', 'AttributedConfigParser', 'configglue', ] configglue-1.1.2/configglue/inischema/attributed.py0000644000175000017500000000501412167562300023532 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### """ AttributtedConfigParser lives here. """ import re from configglue._compat import RawConfigParser marker = object() class ValueWithAttrs(object): """The values returned by AttributtedConfigParser are instances of this. """ def __init__(self, value=marker, **kw): self.value = value self.attrs = kw @property def is_empty(self): """ Is the value unset? Note this is different from being set to None. """ return self.value is marker class AttributedConfigParser(RawConfigParser, object): """Handle attributed ini-style configuration files """ def optionxform(self, optionstr): """See RawConfigParser.optionxform""" return optionstr.lower().replace('-', '_') def normalized_options(self, section): """ Return the section's options, removing the attributes. @param section: The section whose filtered options you want @return: A C{set} of option names, with attributes removed """ return set(re.sub(r'\..*', '', option) for option in self.options(section)) def parse_all(self): """ Go through all sections and options attempting to parse each one. """ for section in self.sections(): for option in self.normalized_options(section): self.parse(section, option) def parse(self, section, option): """Parse a single option in a single section. @param section: the section within which to look for the option @param option: the 'base' option to parse """ if self.has_option(section, option): value = ValueWithAttrs(self.get(section, option)) else: value = ValueWithAttrs() self.set(section, option, value) for opt, val in self.items(section)[:]: if opt.startswith(option + '.'): value.attrs[opt[len(option) + 1:]] = val self.remove_option(section, opt) configglue-1.1.2/configglue/inischema/parsers.py0000644000175000017500000000321112167556061023046 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### """Parsers used by TypedConfigParser live here """ def lines(value): """ Split a string on its newlines RawConfigParser supports "continuations in the style of RFC822", which gives us a very natural way of having values that are lists of strings. If value isn't a string, leaves it alone. """ try: return value.split('\n') except AttributeError: return value _true_values = frozenset(('true', '1', 'on', 'yes')) _false_values = frozenset(('false', '0', 'off', 'no')) def bool_parser(value): """Take a string representation of a boolean and return its boolosity true, 1, on, and yes (in any case) should all be True. false, 0, off, and no (in any case) should all be False. any other string else should raise an error; None and booleans are preserved. """ try: value = value.lower() except AttributeError: return bool(value) else: if value in _true_values: return True if value in _false_values: return False raise ValueError("Unable to determine boolosity of %r" % value) configglue-1.1.2/configglue/tests/0000755000175000017500000000000012167605161020216 5ustar ricardoricardo00000000000000configglue-1.1.2/configglue/tests/test_parser.py0000644000175000017500000014376312167557436023154 0ustar ricardoricardo00000000000000# -*- coding: utf-8 -*- ############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from __future__ import unicode_literals import codecs import os import shutil import tempfile import textwrap import unittest from io import BytesIO from mock import ( MagicMock, Mock, patch, ) from configglue._compat import iteritems from configglue._compat import ( DEFAULTSECT, InterpolationDepthError, InterpolationMissingOptionError, InterpolationSyntaxError, NoOptionError, NoSectionError, ) from configglue.parser import ( CONFIG_FILE_ENCODING, SchemaConfigParser, SchemaValidationError, ) from configglue.schema import ( BoolOption, Section, DictOption, IntOption, ListOption, Schema, StringOption, TupleOption, ) # backwards compatibility if not hasattr(patch, 'object'): # mock < 0.8 from mock import patch_object patch.object = patch_object class TestIncludes(unittest.TestCase): def setUp(self): class MySchema(Schema): foo = StringOption() self.schema = MySchema() fd, self.name = tempfile.mkstemp(suffix='.cfg') os.write(fd, b'[__main__]\nfoo=bar\n') os.close(fd) def tearDown(self): os.remove(self.name) def test_basic_include(self): config = '[__main__]\nincludes=%s' % self.name config = BytesIO(config.encode(CONFIG_FILE_ENCODING)) parser = SchemaConfigParser(self.schema) parser.readfp(config, 'my.cfg') self.assertEquals({'__main__': {'foo': 'bar'}}, parser.values()) def test_locate(self): config = "[__main__]\nincludes=%s" % self.name config = BytesIO(config.encode(CONFIG_FILE_ENCODING)) parser = SchemaConfigParser(self.schema) parser.readfp(config, 'my.cfg') location = parser.locate(option='foo') expected_location = self.name self.assertEqual(expected_location, location) @patch('configglue.parser.logger.warn') @patch('configglue.parser.codecs.open') def test_read_ioerror(self, mock_open, mock_warn): mock_open.side_effect = IOError parser = SchemaConfigParser(self.schema) read_ok = parser.read(self.name) self.assertEqual(read_ok, []) self.assertEqual(mock_warn.call_args_list, [(("File {0} could not be read. Skipping.".format(self.name),), {})]) def test_relative_include(self): """Test parser include files using relative paths.""" def setup_config(): folder = tempfile.mkdtemp() f = codecs.open("%s/first.cfg" % folder, 'w', encoding=CONFIG_FILE_ENCODING) f.write("[__main__]\nfoo=1\nincludes=second.cfg") f.close() f = codecs.open("%s/second.cfg" % folder, 'w', encoding=CONFIG_FILE_ENCODING) f.write("[__main__]\nbar=2\nincludes=sub/third.cfg") f.close() os.mkdir("%s/sub" % folder) f = codecs.open("%s/sub/third.cfg" % folder, 'w', encoding=CONFIG_FILE_ENCODING) f.write("[__main__]\nincludes=../fourth.cfg") f.close() f = codecs.open("%s/fourth.cfg" % folder, 'w', encoding=CONFIG_FILE_ENCODING) f.write("[__main__]\nbaz=3") f.close() config = "[__main__]\nincludes=%s/first.cfg" % folder config = BytesIO(config.encode(CONFIG_FILE_ENCODING)) return config, folder class MySchema(Schema): foo = IntOption() bar = IntOption() baz = IntOption() config, folder = setup_config() expected_values = {'__main__': {'foo': 1, 'bar': 2, 'baz': 3}} parser = SchemaConfigParser(MySchema()) # make sure we start on a clean basedir self.assertEqual(parser._basedir, '') parser.readfp(config, 'my.cfg') self.assertEqual(parser.values(), expected_values) # make sure we leave the basedir clean self.assertEqual(parser._basedir, '') # silently remove any created files try: shutil.rmtree(folder) except: pass def test_local_override(self): """Test parser override values from included files.""" def setup_config(): folder = tempfile.mkdtemp() f = codecs.open("%s/first.cfg" % folder, 'w', encoding=CONFIG_FILE_ENCODING) f.write("[__main__]\nfoo=1\nbar=2\nincludes=second.cfg") f.close() f = codecs.open("%s/second.cfg" % folder, 'w', encoding=CONFIG_FILE_ENCODING) f.write("[__main__]\nbaz=3") f.close() config = "[__main__]\nfoo=4\nincludes=%s/first.cfg" % folder config = BytesIO(config.encode(CONFIG_FILE_ENCODING)) return config, folder class MySchema(Schema): foo = IntOption() bar = IntOption() baz = IntOption() config, folder = setup_config() expected_values = {'__main__': {'foo': 4, 'bar': 2, 'baz': 3}} parser = SchemaConfigParser(MySchema()) # make sure we start on a clean basedir self.assertEqual(parser._basedir, '') parser.readfp(config, 'my.cfg') self.assertEqual(parser.values(), expected_values) # make sure we leave the basedir clean self.assertEqual(parser._basedir, '') # silently remove any created files try: shutil.rmtree(folder) except: pass class TestInterpolation(unittest.TestCase): """Test basic interpolation.""" def test_basic_interpolate(self): class MySchema(Schema): foo = StringOption() bar = BoolOption() config = BytesIO(b'[__main__]\nbar=%(foo)s\nfoo=True') parser = SchemaConfigParser(MySchema()) parser.readfp(config, 'my.cfg') self.assertEquals({'__main__': {'foo': 'True', 'bar': True}}, parser.values()) def test_interpolate_missing_option(self): """Test interpolation with a missing option.""" class MySchema(Schema): foo = StringOption() bar = BoolOption() section = '__main__' option = 'foo' rawval = '%(baz)s' vars = {} parser = SchemaConfigParser(MySchema()) self.assertRaises(InterpolationMissingOptionError, parser._interpolate, section, option, rawval, vars) def test_interpolate_too_deep(self): """Test too deeply recursive interpolation.""" class MySchema(Schema): foo = StringOption() bar = BoolOption() section = '__main__' option = 'foo' rawval = '%(bar)s' vars = {'foo': '%(bar)s', 'bar': '%(foo)s'} parser = SchemaConfigParser(MySchema()) self.assertRaises(InterpolationDepthError, parser._interpolate, section, option, rawval, vars) def test_interpolate_incomplete_format(self): """Test interpolation with incomplete format key.""" class MySchema(Schema): foo = StringOption() bar = BoolOption() section = '__main__' option = 'foo' rawval = '%(bar)' vars = {'foo': '%(bar)s', 'bar': 'pepe'} parser = SchemaConfigParser(MySchema()) self.assertRaises(InterpolationSyntaxError, parser._interpolate, section, option, rawval, vars) def test_interpolate_across_sections(self): """Test interpolation across sections.""" class MySchema(Schema): class foo(Section): bar = IntOption() class baz(Section): wham = IntOption() config = BytesIO(b"[foo]\nbar=%(wham)s\n[baz]\nwham=42") parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertRaises(InterpolationMissingOptionError, parser.get, 'foo', 'bar') def test_interpolate_invalid_key(self): """Test interpolation of invalid key.""" class MySchema(Schema): class foo(Section): bar = IntOption() class baz(Section): wham = IntOption() config = BytesIO(b"[foo]\nbar=%(wham)s\n[baz]\nwham=42") parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertRaises(InterpolationMissingOptionError, parser.get, 'foo', 'bar') @patch('configglue.parser.os') def test_interpolate_environment_basic_syntax(self, mock_os): mock_os.environ = {'PATH': 'foo'} parser = SchemaConfigParser(Schema()) result = parser.interpolate_environment("$PATH") self.assertEqual(result, 'foo') @patch('configglue.parser.os') def test_interpolate_environment_extended_syntax(self, mock_os): mock_os.environ = {'PATH': 'foo'} parser = SchemaConfigParser(Schema()) result = parser.interpolate_environment("${PATH}") self.assertEqual(result, 'foo') @patch('configglue.parser.os') def test_interpolate_environment_with_default_uses_env(self, mock_os): mock_os.environ = {'PATH': 'foo'} parser = SchemaConfigParser(Schema()) result = parser.interpolate_environment("X${PATH:-bar}X") self.assertEqual(result, 'XfooX') @patch('configglue.parser.os') def test_interpolate_environment_with_default_uses_default(self, mock_os): mock_os.environ = {} parser = SchemaConfigParser(Schema()) result = parser.interpolate_environment("X${PATH:-bar}X") self.assertEqual(result, 'XbarX') @patch('configglue.parser.os') def test_interpolate_environment_with_empty_default(self, mock_os): mock_os.environ = {} parser = SchemaConfigParser(Schema()) result = parser.interpolate_environment("X${PATH:-}X") self.assertEqual(result, 'XX') @patch('configglue.parser.os') def test_interpolate_environment_multiple(self, mock_os): mock_os.environ = {'FOO': 'foo', 'BAR': 'bar', 'BAZ': 'baz'} parser = SchemaConfigParser(Schema()) result = parser.interpolate_environment( "$FOO, ${BAR}, ${BAZ:-default}") self.assertEqual(result, 'foo, bar, baz') @patch('configglue.parser.os') def test_interpolate_environment_multiple_with_default(self, mock_os): mock_os.environ = {'FOO': 'foo', 'BAR': 'bar'} parser = SchemaConfigParser(Schema()) result = parser.interpolate_environment( "$FOO, ${BAR}, ${BAZ:-default}") self.assertEqual(result, 'foo, bar, default') def test_interpolate_environment_multiple_defaults(self): "Only the first default is used" parser = SchemaConfigParser(Schema()) result = parser.interpolate_environment( "${FOO:-default1}, ${FOO:-default2}") self.assertEqual(result, 'default1, default1') @patch('configglue.parser.os') def test_interpolate_environment_defaults_nested(self, mock_os): mock_os.environ = {'BAR': 'bar'} parser = SchemaConfigParser(Schema()) result = parser.interpolate_environment("${FOO:-$BAR}") self.assertEqual(result, 'bar') @patch('configglue.parser.os') @patch('configglue.parser.re.compile') def test_interpolate_environment_default_loop(self, mock_compile, mock_os): mock_os.environ = {'FOO': 'foo'} parser = SchemaConfigParser(Schema()) mock_match = MagicMock() mock_match.group.return_value = "FOO" mock_compile.return_value.search.return_value = mock_match result = parser.interpolate_environment("${FOO:-bar}") # should be uninterpolated result self.assertEqual(result, '${FOO:-bar}') @patch('configglue.parser.os') def test_interpolate_environment_in_config(self, mock_os): mock_os.environ = {'PYTHONPATH': 'foo', 'PATH': 'bar'} class MySchema(Schema): pythonpath = StringOption() path = StringOption() config = BytesIO(b"[__main__]\npythonpath=${PYTHONPATH}\npath=$PATH") parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertEqual(parser.values('__main__'), {'pythonpath': 'foo', 'path': 'bar'}) def test_interpolate_environment_without_keys(self): parser = SchemaConfigParser(Schema()) rawval = "['%H:%M:%S', '%Y-%m-%d']" value = parser.interpolate_environment(rawval) self.assertEqual(value, rawval) @patch('configglue.parser.os') def test_get_with_environment_var(self, mock_os): mock_os.environ = {'FOO': '42'} class MySchema(Schema): foo = IntOption() config = BytesIO(b"[__main__]\nfoo=$FOO") parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertEqual(parser.get('__main__', 'foo'), 42) def test_get_without_environment_var(self): class MySchema(Schema): foo = IntOption() config = BytesIO(b"[__main__]\nfoo=$FOO") parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertEqual(parser.get('__main__', 'foo'), 0) def test_get_interpolation_keys_string(self): """Test get_interpolation_keys for a string.""" class MySchema(Schema): foo = StringOption() config = BytesIO(b"[__main__]\nfoo=%(bar)s") expected = ('%(bar)s', set(['bar'])) parser = SchemaConfigParser(MySchema()) parser.readfp(config) result = parser._get_interpolation_keys('__main__', 'foo') self.assertEqual(result, expected) def test_get_interpolation_keys_int(self): """Test get_interpolation_keys for an integer.""" class MySchema(Schema): foo = IntOption() config = BytesIO(b"[__main__]\nfoo=%(bar)s") expected = ('%(bar)s', set(['bar'])) parser = SchemaConfigParser(MySchema()) parser.readfp(config) result = parser._get_interpolation_keys('__main__', 'foo') self.assertEqual(result, expected) def test_get_interpolation_keys_bool(self): """Test get_interpolation_keys for a boolean.""" class MySchema(Schema): foo = BoolOption() config = BytesIO(b"[__main__]\nfoo=%(bar)s") expected = ('%(bar)s', set(['bar'])) parser = SchemaConfigParser(MySchema()) parser.readfp(config) result = parser._get_interpolation_keys('__main__', 'foo') self.assertEqual(result, expected) def test_get_interpolation_keys_tuple(self): """Test get_interpolation_keys for a tuple.""" class MySchema(Schema): foo = TupleOption(2) config = BytesIO(b"[__main__]\nfoo=%(bar)s,%(baz)s") expected = ('%(bar)s,%(baz)s', set(['bar', 'baz'])) parser = SchemaConfigParser(MySchema()) parser.readfp(config) result = parser._get_interpolation_keys('__main__', 'foo') self.assertEqual(result, expected) def test_get_interpolation_keys_lines(self): """Test get_interpolation_keys for a list.""" class MySchema(Schema): foo = ListOption(item=StringOption()) config = BytesIO(b"[__main__]\nfoo=%(bar)s\n %(baz)s") expected = ('%(bar)s\n%(baz)s', set(['bar', 'baz'])) parser = SchemaConfigParser(MySchema()) parser.readfp(config) result = parser._get_interpolation_keys('__main__', 'foo') self.assertEqual(result, expected) def test_get_interpolation_keys_tuple_lines(self): """Test get_interpolation_keys for a list of tuples.""" class MySchema(Schema): foo = ListOption(item=TupleOption(2)) config = BytesIO( b"[__main__]\nfoo=%(bar)s,%(bar)s\n %(baz)s,%(baz)s") expected = ('%(bar)s,%(bar)s\n%(baz)s,%(baz)s', set(['bar', 'baz'])) parser = SchemaConfigParser(MySchema()) parser.readfp(config) result = parser._get_interpolation_keys('__main__', 'foo') self.assertEqual(result, expected) def test_get_interpolation_keys_dict(self): """Test get_interpolation_keys for a dict.""" class MySchema(Schema): foo = DictOption(spec={'a': IntOption()}) config = BytesIO(textwrap.dedent(""" [__noschema__] bar=4 [__main__] foo=mydict [mydict] a=%(bar)s """).encode(CONFIG_FILE_ENCODING)) expected = ('mydict', set([])) parser = SchemaConfigParser(MySchema()) parser.readfp(config) result = parser._get_interpolation_keys('__main__', 'foo') self.assertEqual(result, expected) def test_interpolate_value_duplicate_key(self): """Test interpolate_value for a duplicate key.""" class MySchema(Schema): foo = TupleOption(2) config = BytesIO( b"[__noschema__]\nbar=4\n[__main__]\nfoo=%(bar)s,%(bar)s") expected_value = '4,4' parser = SchemaConfigParser(MySchema()) parser.readfp(config) value = parser._interpolate_value('__main__', 'foo') self.assertEqual(value, expected_value) def test_interpolate_value_invalid_key(self): """Test interpolate_value with an invalid key.""" class MySchema(Schema): foo = TupleOption(2) config = BytesIO(b"[other]\nbar=4\n[__main__]\nfoo=%(bar)s,%(bar)s") expected_value = None parser = SchemaConfigParser(MySchema()) parser.readfp(config) value = parser._interpolate_value('__main__', 'foo') self.assertEqual(value, expected_value) def test_interpolate_value_no_keys(self): """Test interpolate_value with no keys.""" class MySchema(Schema): foo = TupleOption(2) config = BytesIO(b"[__main__]\nfoo=%(bar)s,%(bar)s") mock_get_interpolation_keys = Mock(return_value=('%(bar)s', None)) parser = SchemaConfigParser(MySchema()) parser.readfp(config) with patch.object(parser, '_get_interpolation_keys', mock_get_interpolation_keys): value = parser._interpolate_value('__main__', 'foo') self.assertEqual(value, None) def test_get_with_raw_value(self): """Test get using a raw value.""" class MySchema(Schema): foo = StringOption(raw=True) config = BytesIO(b'[__main__]\nfoo=blah%(asd)##$@@dddf2kjhkjs') expected_value = 'blah%(asd)##$@@dddf2kjhkjs' parser = SchemaConfigParser(MySchema()) parser.readfp(config) value = parser.get('__main__', 'foo') self.assertEqual(value, expected_value) def test_interpolate_parse_dict(self): """Test interpolation while parsing a dict.""" class MySchema(Schema): foo = DictOption(spec={'a': IntOption()}) config = BytesIO(textwrap.dedent(""" [__noschema__] bar=4 [__main__] foo=mydict [mydict] a=%(bar)s """).encode(CONFIG_FILE_ENCODING)) expected = {'__main__': {'foo': {'a': 4}}} parser = SchemaConfigParser(MySchema()) parser.readfp(config) result = parser.values() self.assertEqual(result, expected) class TestSchemaConfigParser(unittest.TestCase): def setUp(self): class MySchema(Schema): foo = StringOption() self.schema = MySchema() self.parser = SchemaConfigParser(self.schema) self.config = BytesIO(b"[__main__]\nfoo = bar") def test_init_no_args(self): self.assertRaises(TypeError, SchemaConfigParser) def test_init_valid_schema(self): self.assertEqual(self.parser.schema, self.schema) def test_init_invalid_schema(self): class MyInvalidSchema(Schema): class __main__(Section): pass self.assertRaises(SchemaValidationError, SchemaConfigParser, MyInvalidSchema()) def test_items(self): self.parser.readfp(self.config) items = self.parser.items('__main__') self.assertEqual(set(items), set([('foo', 'bar')])) def test_items_no_section(self): self.assertRaises(NoSectionError, self.parser.items, '__main__') def test_items_raw(self): config = BytesIO(b'[__main__]\nfoo=%(baz)s') self.parser.readfp(config) items = self.parser.items('__main__', raw=True) self.assertEqual(set(items), set([('foo', '%(baz)s')])) def test_items_vars(self): config = BytesIO(b'[__main__]\nfoo=%(baz)s') self.parser.readfp(config) items = self.parser.items('__main__', vars={'baz': '42'}) self.assertEqual(set(items), set([('foo', '42'), ('baz', '42')])) def test_items_interpolate(self): """Test parser.items with interpolated values.""" class MySchema(Schema): foo = StringOption() class baz(Section): bar = StringOption() parser = SchemaConfigParser(MySchema()) config = BytesIO(b'[__main__]\nfoo=%(bar)s\n[baz]\nbar=42') parser.readfp(config) # test interpolate items = parser.items('baz') self.assertEqual(items, list(iteritems({'bar': '42'}))) def test_items_interpolate_error(self): config = BytesIO(b'[__main__]\nfoo=%(bar)s') self.parser.readfp(config) self.assertRaises(InterpolationMissingOptionError, self.parser.items, '__main__') def test_values_empty_parser(self): values = self.parser.values() self.assertEqual(values, {'__main__': {'foo': ''}}) def test_values_full_parser(self): expected_values = {'__main__': {'foo': 'bar'}} self.parser.readfp(self.config) values = self.parser.values() self.assertEqual(expected_values, values) values = self.parser.values(section='__main__') self.assertEqual(expected_values['__main__'], values) def test_values_many_sections_same_option(self): """Test parser.values for many section with the same option.""" class MySchema(Schema): class foo(Section): bar = IntOption() class baz(Section): bar = IntOption() config = BytesIO(b"[foo]\nbar=3\n[baz]\nbar=4") expected_values = {'foo': {'bar': 3}, 'baz': {'bar': 4}} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) values = parser.values() self.assertEqual(values, expected_values) def test_values_many_sections_different_options(self): """Test parser.values for many sections with different options.""" class MySchema(Schema): class foo(Section): bar = IntOption() class bar(Section): baz = IntOption() config = BytesIO(b"[foo]\nbar=3\n[bar]\nbaz=4") expected_values = {'foo': {'bar': 3}, 'bar': {'baz': 4}} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) values = parser.values() self.assertEqual(values, expected_values) def test_parse_option(self): """Test parser parses option.""" class MyOtherSchema(Schema): class foo(Section): bar = StringOption() expected_value = 'baz' config = BytesIO(b"[foo]\nbar = baz") parser = SchemaConfigParser(MyOtherSchema()) parser.readfp(config) value = parser.get('foo', 'bar') self.assertEqual(value, expected_value) def test_parse_invalid_section(self): self.assertRaises(NoSectionError, self.parser.parse, 'bar', 'baz', '1') def test_default_values(self): """Test parser reads default option values.""" class MySchema(Schema): foo = BoolOption(default=True) class bar(Section): baz = IntOption() bla = StringOption(default='hello') schema = MySchema() config = BytesIO(b"[bar]\nbaz=123") expected_values = {'__main__': {'foo': True}, 'bar': {'baz': 123, 'bla': 'hello'}} parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEquals(expected_values, parser.values()) config = BytesIO(b"[bar]\nbla=123") expected = { '__main__': {'foo': True}, 'bar': {'baz': 0, 'bla': '123'}} parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEquals(expected, parser.values()) def test_fatal_options(self): """Test parsing non-provided options marked as fatal.""" class MySchema(Schema): foo = IntOption(fatal=True) bar = IntOption() schema = MySchema() config = BytesIO(b"[__main__]\nfoo=123") expected = {'__main__': {'foo': 123, 'bar': 0}} parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEquals(expected, parser.values()) config = BytesIO(b"[__main__]\nbar=123") parser = SchemaConfigParser(schema) parser.readfp(config) self.assertRaises(NoOptionError, parser.values) def test_extra_sections(self): """Test extra_sections.""" class MySchema(Schema): foo = DictOption(spec={'bar': IntOption()}) config = BytesIO(b"[__main__]\nfoo=mydict\n[mydict]\nbar=1") parser = SchemaConfigParser(MySchema()) parser.readfp(config) parser.parse_all() expected_sections = set(['mydict']) extra_sections = parser.extra_sections self.assertEqual(expected_sections, extra_sections) def test_extra_sections_dict_default_value(self): """Test parse dict with default value.""" class MySchema(Schema): foo = DictOption(spec={ 'bar': IntOption(), 'baz': BoolOption()}) parser = SchemaConfigParser(MySchema()) self.assertEqual(parser.get('__main__', 'foo'), {'bar': 0, 'baz': False}) self.assertEqual(parser.extra_sections, set([])) def test_extra_sections_missing_section(self): """Test parse dict with missing referenced section.""" class MySchema(Schema): foo = DictOption() config = BytesIO(textwrap.dedent(""" [__main__] foo = dict1 """).encode(CONFIG_FILE_ENCODING)) parser = SchemaConfigParser(MySchema()) parser.readfp(config) parser.parse_all() self.assertEqual(parser.extra_sections, set(['dict1'])) def test_multiple_extra_sections(self): """Test parsing multiple extra sections.""" class MySchema(Schema): foo = ListOption( item=DictOption(spec={'bar': IntOption()})) config = BytesIO(b'[__main__]\nfoo=d1\n d2\n d3\n' b'[d1]\nbar=1\n[d2]\nbar=2\n[d3]\nbar=3') parser = SchemaConfigParser(MySchema()) parser.readfp(config) parser.parse_all() expected_sections = set(['d1', 'd2', 'd3']) extra_sections = parser.extra_sections self.assertEqual(expected_sections, extra_sections) def test_get_default(self): config = BytesIO(b"[__main__]\n") expected = '' self.parser.readfp(config) default = self.parser._get_default('__main__', 'foo') self.assertEqual(default, expected) def test_get_default_noschema(self): config = BytesIO(b"[__noschema__]\nbar=1\n[__main__]\n") expected = '1' self.parser.readfp(config) default = self.parser._get_default('__noschema__', 'bar') self.assertEqual(default, expected) def test_get_default_from_section(self): """Test parser._get_default for a section/option pair.""" class MySchema(Schema): class foo(Section): bar = IntOption() config = BytesIO(b"[__main__]\n") expected = 0 parser = SchemaConfigParser(MySchema()) parser.readfp(config) default = parser._get_default('foo', 'bar') self.assertEqual(default, expected) def test_get_default_no_option(self): self.assertRaises(NoOptionError, self.parser._get_default, '__main__', 'bar') def test_get_default_no_section(self): self.assertRaises(NoSectionError, self.parser._get_default, 'foo', 'bar') def test_multi_file_dict_config(self): """Test parsing a dict option spanning multiple files.""" class MySchema(Schema): foo = DictOption(spec={ 'bar': IntOption(), 'baz': IntOption(), }, strict=True) config1 = BytesIO(b'[__main__]\nfoo=mydict\n[mydict]\nbar=1\nbaz=1') config2 = BytesIO(b'[mydict]\nbaz=2') expected_values = {'__main__': {'foo': {'bar': 1, 'baz': 2}}} parser = SchemaConfigParser(MySchema()) parser.readfp(config1) parser.readfp(config2) self.assertEqual(parser.values(), expected_values) def test_multi_file_dict_list_config(self): """Test parsing a list of dicts option spanning multiple files.""" class MySchema(Schema): foo = ListOption( item=DictOption(spec={ 'bar': IntOption(), 'baz': IntOption(), }, strict=True)) config1 = BytesIO(b'[__main__]\nfoo=mydict\n[mydict]\nbar=1\nbaz=1') expected_values = {'__main__': {'foo': [{'bar': 1, 'baz': 1}]}} parser = SchemaConfigParser(MySchema()) parser.readfp(config1) self.assertEqual(parser.values(), expected_values) # override used dictionaries config2 = BytesIO(b'[__main__]\nfoo=otherdict\n[otherdict]\nbar=2') expected_values = {'__main__': {'foo': [{'bar': 2, 'baz': 0}]}} parser.readfp(config2) self.assertEqual(parser.values(), expected_values) # override existing dictionaries config3 = BytesIO(b'[otherdict]\nbaz=3') expected_values = {'__main__': {'foo': [{'bar': 2, 'baz': 3}]}} parser.readfp(config3) self.assertEqual(parser.values(), expected_values) # reuse existing dict config4 = BytesIO(b'[__main__]\nfoo=mydict\n otherdict') expected_values = {'__main__': {'foo': [{'bar': 1, 'baz': 1}, {'bar': 2, 'baz': 3}]}} parser.readfp(config4) self.assertEqual(parser.values(), expected_values) def test_read_multiple_files(self): def setup_config(): folder = tempfile.mkdtemp() f = codecs.open("%s/first.cfg" % folder, 'w', encoding=CONFIG_FILE_ENCODING) f.write("[__main__]\nfoo=foo") f.close() f = codecs.open("%s/second.cfg" % folder, 'w', encoding=CONFIG_FILE_ENCODING) f.write("[__main__]\nfoo=bar") f.close() files = ["%s/first.cfg" % folder, "%s/second.cfg" % folder] return files, folder files, folder = setup_config() self.parser.read(files) self.assertEqual(self.parser.values(), {'__main__': {'foo': 'bar'}}) # silently remove any created files try: shutil.rmtree(folder) except: pass def test_read_utf8_encoded_file(self): # create config file fp, filename = tempfile.mkstemp() try: f = codecs.open(filename, 'w', encoding=CONFIG_FILE_ENCODING) f.write('[__main__]\nfoo=€') f.close() self.parser.read(filename) self.assertEqual(self.parser.values(), {'__main__': {'foo': '€'}}) finally: # destroy config file os.remove(filename) def test_readfp_with_utf8_encoded_text(self): config = BytesIO('[__main__]\nfoo=€'.encode(CONFIG_FILE_ENCODING)) self.parser.readfp(config) self.assertEqual(self.parser.values(), {'__main__': {'foo': '€'}}) def test_set(self): with tempfile.NamedTemporaryFile() as f: f.write(b'[__main__]\nfoo=1') f.flush() self.parser.read(f.name) self.assertEqual(self.parser._dirty, {}) self.assertEqual(self.parser.get('__main__', 'foo'), '1') self.parser.set('__main__', 'foo', '2') self.assertEqual(self.parser.get('__main__', 'foo'), '2') self.assertEqual(self.parser._dirty, {f.name: {'__main__': {'foo': '2'}}}) def test_set_non_string(self): """Test parser.set with a non-string value.""" class MySchema(Schema): foo = IntOption() bar = BoolOption() parser = SchemaConfigParser(MySchema()) parser.parse_all() parser.set('__main__', 'foo', 2) parser.set('__main__', 'bar', False) self.assertEqual(parser.get('__main__', 'foo'), 2) self.assertEqual(parser._sections['__main__']['foo'], '2') self.assertEqual(parser.get('__main__', 'bar'), False) self.assertEqual(parser._sections['__main__']['bar'], 'False') def test_set_invalid_type(self): self.parser.parse_all() self.assertRaises(TypeError, self.parser.set, '__main__', 'foo', 2) def test_write(self): """Test parser write config to a file.""" class MySchema(Schema): foo = StringOption() class DEFAULTSECT(Section): pass parser = SchemaConfigParser(MySchema()) expected = "[{0}]\nbaz = 2\n\n[__main__]\nfoo = bar".format( DEFAULTSECT) config = BytesIO(expected.encode(CONFIG_FILE_ENCODING)) parser.readfp(config) # create config file fp, filename = tempfile.mkstemp() try: parser.write(codecs.open(filename, 'w', encoding=CONFIG_FILE_ENCODING)) result = codecs.open(filename, 'r', encoding=CONFIG_FILE_ENCODING).read().strip() self.assertEqual(result, expected) finally: # remove the file os.unlink(filename) def test_write_prefill_parser(self): """Test parser write config to a file.""" class MySchema(Schema): foo = IntOption() parser = SchemaConfigParser(MySchema()) expected = "[__main__]\nfoo = 0" # create config file fp, filename = tempfile.mkstemp() try: parser.write(codecs.open(filename, 'w', encoding=CONFIG_FILE_ENCODING)) result = codecs.open(filename, 'r', encoding=CONFIG_FILE_ENCODING).read().strip() self.assertEqual(result, expected) finally: # remove the file os.unlink(filename) def test_save_config(self): expected = '[__main__]\nfoo = 42' self._check_save_file(expected) def test_save_config_non_ascii(self): expected = '[__main__]\nfoo = fóobâr' self._check_save_file(expected) def _check_save_file(self, expected, read_config=True): config = BytesIO(expected.encode(CONFIG_FILE_ENCODING)) if read_config: self.parser.readfp(config) # create config file fp, filename = tempfile.mkstemp() try: self.parser.save(codecs.open(filename, 'w', encoding=CONFIG_FILE_ENCODING)) result = codecs.open(filename, 'r', encoding=CONFIG_FILE_ENCODING).read().strip() self.assertEqual(result, expected) self.parser.save(filename) result = codecs.open(filename, 'r', encoding=CONFIG_FILE_ENCODING).read().strip() self.assertEqual(result, expected) finally: # remove the file os.unlink(filename) def test_save_config_prefill_parser(self): """Test parser save config when no config files read.""" expected = '[__main__]\nfoo =' self._check_save_file(expected, read_config=False) def test_save_no_config_same_files(self): class MySchema(Schema): foo = IntOption() parser = SchemaConfigParser(MySchema()) parser.set('__main__', 'foo', 2) self.assertRaises(ValueError, parser.save) def test_save_config_same_files(self): """Test parser save config values to original files.""" def setup_config(): folder = tempfile.mkdtemp() f = codecs.open("%s/first.cfg" % folder, 'w', encoding=CONFIG_FILE_ENCODING) f.write("[__main__]\nfoo=1") f.close() f = codecs.open("%s/second.cfg" % folder, 'w', encoding=CONFIG_FILE_ENCODING) f.write("[__main__]\nbar=2") f.close() files = ["%s/first.cfg" % folder, "%s/second.cfg" % folder] return files, folder class MySchema(Schema): foo = StringOption() bar = StringOption() baz = IntOption() self.parser = SchemaConfigParser(MySchema()) files, folder = setup_config() self.parser.read(files) self.parser.set('__main__', 'foo', '42') self.parser.set('__main__', 'bar', '42') self.parser.set('__main__', 'baz', 42) self.parser.save() # test the changes were correctly saved data = codecs.open("%s/first.cfg" % folder, encoding=CONFIG_FILE_ENCODING).read() self.assertTrue('foo = 42' in data) self.assertFalse('bar = 42' in data) # new value goes into last read config file self.assertFalse('baz = 42' in data) data = codecs.open("%s/second.cfg" % folder, encoding=CONFIG_FILE_ENCODING).read() self.assertFalse('foo = 42' in data) self.assertTrue('bar = 42' in data) # new value goes into last read config file self.assertTrue('baz = 42' in data) # silently remove any created files try: shutil.rmtree(folder) except: pass def test_save_config_last_location_nested_includes(self): def setup_config(): folder = tempfile.mkdtemp() f = codecs.open("%s/first.cfg" % folder, 'w', encoding=CONFIG_FILE_ENCODING) f.write("[__main__]\nfoo=1") f.close() f = codecs.open("%s/second.cfg" % folder, 'w', encoding=CONFIG_FILE_ENCODING) f.write("[__main__]\nbar=2\nincludes = third.cfg") f.close() f = codecs.open("%s/third.cfg" % folder, 'w', encoding=CONFIG_FILE_ENCODING) f.write("[__main__]\nfoo=3") f.close() files = ["%s/first.cfg" % folder, "%s/second.cfg" % folder] return files, folder class MySchema(Schema): foo = StringOption() bar = StringOption() baz = IntOption() self.parser = SchemaConfigParser(MySchema()) files, folder = setup_config() self.parser.read(files) self.parser.set('__main__', 'foo', '42') self.parser.set('__main__', 'bar', '42') self.parser.set('__main__', 'baz', 42) self.parser.save() # test the changes were correctly saved data = codecs.open("%s/first.cfg" % folder, encoding=CONFIG_FILE_ENCODING).read() self.assertEqual(data.strip(), '[__main__]\nfoo=1') data = codecs.open("%s/third.cfg" % folder, encoding=CONFIG_FILE_ENCODING).read() self.assertEqual(data.strip(), '[__main__]\nfoo = 42') data = codecs.open("%s/second.cfg" % folder, encoding=CONFIG_FILE_ENCODING).read() self.assertTrue('bar = 42' in data) # new value goes into last read config file # not in the last included config file self.assertTrue('baz = 42' in data) # silently remove any created files try: shutil.rmtree(folder) except: pass class TestParserIsValid(unittest.TestCase): def setUp(self): class MySchema(Schema): foo = StringOption() self.schema = MySchema() self.parser = SchemaConfigParser(self.schema) self.config = BytesIO(b"[__main__]\nfoo = bar") def test_basic_is_valid(self): """Test basic validation without error reporting.""" class MySchema(Schema): foo = IntOption() schema = MySchema() config = BytesIO(b"[__main__]\nfoo = 5") parser = SchemaConfigParser(schema) parser.readfp(config) self.assertTrue(parser.is_valid()) def test_basic_is_valid_with_report(self): """Test basic validation with error reporting.""" class MySchema(Schema): foo = IntOption() config = BytesIO(b"[__main__]\nfoo=5") expected = (True, []) parser = SchemaConfigParser(MySchema()) parser.readfp(config) valid, errors = parser.is_valid(report=True) self.assertEqual((valid, errors), expected) def test_basic_is_not_valid(self): """Test invalid config without error reporting.""" class MySchema(Schema): foo = IntOption() schema = MySchema() config = BytesIO(b"[__main__]\nfoo = 5\nbar = 6") parser = SchemaConfigParser(schema) parser.readfp(config) self.assertFalse(parser.is_valid()) def test_basic_is_not_valid_with_report(self): """Test invalid config with error reporting.""" class MySchema(Schema): foo = IntOption() config = BytesIO(b"[__main__]\nfoo=5\nbar=6") errors = ["Configuration includes invalid options for " "section '__main__': bar"] expected = (False, errors) parser = SchemaConfigParser(MySchema()) parser.readfp(config) valid, errors = parser.is_valid(report=True) self.assertEqual((valid, errors), expected) def test_is_not_valid_parser_error(self): """Test parser.is_valid when parser errors.""" class MySchema(Schema): foo = IntOption() def mock_parse_all(self): assert False schema = MySchema() config = BytesIO(b"[__main__]\nfoo = 5") parser = SchemaConfigParser(schema) parser.parse_all = mock_parse_all parser.readfp(config) self.assertFalse(parser.is_valid()) def test_parse_invalid_section(self): config = BytesIO(b"[bar]\nbaz=foo") self.parser.readfp(config) self.assertFalse(self.parser.is_valid()) def test_parse_invalid_section_with_report(self): config = BytesIO(b"[bar]\nbaz=foo") self.parser.readfp(config) valid, errors = self.parser.is_valid(report=True) self.assertFalse(valid) self.assertEqual(errors[0], 'Sections in configuration are missing from schema: bar') def test_different_sections(self): config = BytesIO(b"[__main__]\nfoo=1\n[bar]\nbaz=2") self.parser.readfp(config) self.assertFalse(self.parser.is_valid()) def test_missing_fatal_options(self): """Test parser.is_valid when missing fatal options.""" class MySchema(Schema): foo = IntOption() bar = IntOption(fatal=True) config = BytesIO(b"[__main__]\nfoo=1") parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertFalse(parser.is_valid()) def test_missing_nonfatal_options(self): """Test parser.is_valid when missing non-fatal options.""" class MySchema(Schema): foo = IntOption() bar = IntOption(fatal=True) config = BytesIO(b"[__main__]\nbar=2") parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertTrue(parser.is_valid()) def test_extra_sections(self): """Test parser.is_valid with extra sections.""" class MySchema(Schema): foo = DictOption(spec={'bar': IntOption()}) config = BytesIO(b"[__main__]\nfoo=mydict\n[mydict]\nbar=1") parser = SchemaConfigParser(MySchema()) parser.readfp(config) parser.parse_all() self.assertTrue(parser.is_valid()) def test_extra_sections_when_dict_with_nested_dicts(self): """Test parser.is_valid with extra sections in a nested dict.""" class MySchema(Schema): foo = DictOption(item=DictOption()) config = BytesIO(b""" [__main__] foo=dict1 [dict1] bar=dict2 [dict2] baz=42 """) parser = SchemaConfigParser(MySchema()) parser.readfp(config) parser.parse_all() self.assertEqual(parser.values(), {'__main__': {'foo': {'bar': {'baz': '42'}}}}) self.assertTrue(parser.is_valid()) def test_extra_sections_with_nested_dicts_strict(self): """Test parser.is_valid w/ extra sections in a nested dict (strict).""" class MySchema(Schema): foo = DictOption(spec={'bar': DictOption()}, strict=True) config = BytesIO(b""" [__main__] foo=dict1 [dict1] bar=dict2 [dict2] baz=42 """) parser = SchemaConfigParser(MySchema()) parser.readfp(config) parser.parse_all() self.assertEqual(parser.values(), {'__main__': {'foo': {'bar': {'baz': '42'}}}}) self.assertTrue(parser.is_valid()) def test_extra_sections_when_lines_dict_with_nested_dicts(self): """Test parser.is_valid w/ extra section in list of nested dicts.""" class MySchema(Schema): foo = ListOption( item=DictOption(item=DictOption())) config = BytesIO(b""" [__main__] foo = dict1 dict2 [dict1] bar = dict3 [dict2] baz = dict4 [dict3] wham = 1 [dict4] whaz = 2 """) parser = SchemaConfigParser(MySchema()) parser.readfp(config) parser.parse_all() self.assertEqual(parser.values(), {'__main__': {'foo': [ {'bar': {'wham': '1'}}, {'baz': {'whaz': '2'}}]}}) self.assertTrue(parser.is_valid()) def test_extra_sections_when_dict_with_nested_lines_dicts(self): """Test parser.is_valid in dict of nested list lists.""" class MySchema(Schema): foo = DictOption( item=ListOption(item=DictOption())) config = BytesIO(b""" [__main__] foo = dict1 [dict1] bar = dict2 dict3 [dict2] baz = 1 [dict3] wham = 2 """) parser = SchemaConfigParser(MySchema()) parser.readfp(config) parser.parse_all() self.assertEqual(parser.values(), {'__main__': {'foo': {'bar': [{'baz': '1'}, {'wham': '2'}]}}}) self.assertTrue(parser.is_valid()) def test_extra_sections_when_lines_dict_with_nested_lines_dicts(self): """Test parser.is_valid in dict of nested dict lists.""" class MySchema(Schema): foo = ListOption( item=DictOption( item=ListOption(item=DictOption()))) config = BytesIO(b""" [__main__] foo = dict1 dict2 [dict1] bar = dict3 dict4 [dict2] baz = dict5 dict6 [dict3] wham = 1 [dict4] whaz = 2 [dict5] whoosh = 3 [dict6] swoosh = 4 """) parser = SchemaConfigParser(MySchema()) parser.readfp(config) parser.parse_all() self.assertEqual(parser.values(), {'__main__': {'foo': [ {'bar': [{'wham': '1'}, {'whaz': '2'}]}, {'baz': [{'whoosh': '3'}, {'swoosh': '4'}]}]}}) self.assertTrue(parser.is_valid()) def test_extra_sections_with_missing_section(self): """Test parser.is_valid with dict referencing missing section.""" class MySchema(Schema): foo = DictOption() config = BytesIO(textwrap.dedent(""" [__main__] foo = dict1 """).encode(CONFIG_FILE_ENCODING)) parser = SchemaConfigParser(MySchema()) parser.readfp(config) parser.parse_all() self.assertRaises(NoSectionError, parser.values) # config is not valid self.assertFalse(parser.is_valid()) def test_multiple_extra_sections(self): """Test parser.is_valid with multiple extra sections.""" class MySchema(Schema): foo = ListOption( item=DictOption(spec={'bar': IntOption()})) config = BytesIO(b'[__main__]\nfoo=d1\n d2\n d3\n' b'[d1]\nbar=1\n[d2]\nbar=2\n[d3]\nbar=3') parser = SchemaConfigParser(MySchema()) parser.readfp(config) parser.parse_all() self.assertTrue(parser.is_valid()) def test_noschema_section(self): config = BytesIO( b"[__main__]\nfoo=%(bar)s\n[__noschema__]\nbar=hello") parser = SchemaConfigParser(self.schema) parser.readfp(config) parser.parse_all() self.assertTrue(parser.is_valid()) def test_missing_schema_sections(self): class MySchema(Schema): class foo(Section): bar = IntOption() class bar(Section): baz = BoolOption() config = BytesIO(textwrap.dedent(""" [foo] bar = 3 """).encode(CONFIG_FILE_ENCODING)) parser = SchemaConfigParser(MySchema()) parser.readfp(config) parser.parse_all() self.assertTrue(parser.is_valid()) if __name__ == '__main__': unittest.main() configglue-1.1.2/configglue/tests/__init__.py0000644000175000017500000000104712167556115022335 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### configglue-1.1.2/configglue/tests/test_schemaconfig.py0000644000175000017500000005056112167557767024306 0ustar ricardoricardo00000000000000# -*- coding: utf-8 -*- ############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from __future__ import unicode_literals import unittest import os import sys from io import BytesIO, StringIO from optparse import ( OptionConflictError, OptionParser, ) from mock import ( Mock, patch, ) from configglue._compat import PY2 from configglue._compat import NoSectionError from configglue.glue import ( configglue, schemaconfigglue, ) from configglue.parser import SchemaConfigParser from configglue.schema import ( DictOption, IntOption, Option, Schema, Section, StringOption, ) # backwards compatibility if not hasattr(patch, 'object'): # mock < 0.8 from mock import patch_object patch.object = patch_object class TestOption(unittest.TestCase): cls = Option def test_repr_name(self): """Test Option repr with name.""" opt = self.cls() expected = "<{0}>".format(self.cls.__name__) self.assertEqual(repr(opt), expected) opt = self.cls(name='name') expected = "<{0} name>".format(self.cls.__name__) self.assertEqual(repr(opt), expected) sect = Section(name='sect') opt = self.cls(name='name', section=sect) expected = "<{0} sect.name>".format(self.cls.__name__) self.assertEqual(repr(opt), expected) def test_repr_extra(self): """Test Option repr with other attributes.""" opt = self.cls(name='name', raw=True) expected = "<{0} name raw>".format(self.cls.__name__) self.assertEqual(repr(opt), expected) opt = self.cls(name='name', fatal=True) expected = "<{0} name fatal>".format(self.cls.__name__) self.assertEqual(repr(opt), expected) opt = self.cls(name='name', raw=True, fatal=True) expected = "<{0} name raw fatal>".format(self.cls.__name__) self.assertEqual(repr(opt), expected) def test_parse(self): """Test Option parse.""" opt = self.cls() self.assertRaises(NotImplementedError, opt.parse, '') def test_equal(self): """Test Option equality.""" opt1 = self.cls() opt2 = self.cls(name='name', raw=True) self.assertEqual(opt1, self.cls()) self.assertEqual(opt2, self.cls(name='name', raw=True)) self.assertNotEqual(opt1, opt2) self.assertNotEqual(opt1, None) class TestSection(unittest.TestCase): cls = Section def test_repr_name(self): """Test Section repr method.""" sect = self.cls() expected = "<{0}>".format(self.cls.__name__) self.assertEqual(repr(sect), expected) sect = self.cls(name='sect') expected = "<{0} sect>".format(self.cls.__name__) self.assertEqual(repr(sect), expected) def test_equal(self): """Test Section equality.""" sec1 = self.cls() sec2 = self.cls(name='sec2') self.assertEqual(sec1, self.cls()) self.assertEqual(sec2, self.cls(name='sec2')) self.assertNotEqual(sec1, sec2) def test_has_option(self): """Test Section has_option method.""" class MySection(self.cls): foo = IntOption() sec1 = MySection() self.assertTrue(sec1.has_option('foo')) self.assertFalse(sec1.has_option('bar')) class TestSchemaConfigGlue(unittest.TestCase): def setUp(self): class MySchema(Schema): class foo(Section): bar = IntOption() baz = IntOption(help='The baz option') self.parser = SchemaConfigParser(MySchema()) def test_glue_no_op(self): """Test schemaconfigglue with the default OptionParser value.""" config = BytesIO(b"[__main__]\nbaz=1") self.parser.readfp(config) self.assertEqual(self.parser.values(), {'foo': {'bar': 0}, '__main__': {'baz': 1}}) op, options, args = schemaconfigglue(self.parser, argv=['--baz', '2']) self.assertEqual(self.parser.values(), {'foo': {'bar': 0}, '__main__': {'baz': 2}}) def test_glue_no_argv(self): """Test schemaconfigglue with the default argv value.""" config = BytesIO(b"[__main__]\nbaz=1") self.parser.readfp(config) self.assertEqual(self.parser.values(), {'foo': {'bar': 0}, '__main__': {'baz': 1}}) _argv, sys.argv = sys.argv, [] try: op, options, args = schemaconfigglue(self.parser) self.assertEqual(self.parser.values(), {'foo': {'bar': 0}, '__main__': {'baz': 1}}) finally: sys.argv = _argv def test_glue_section_option(self): """Test schemaconfigglue overriding one option.""" config = BytesIO(b"[foo]\nbar=1") self.parser.readfp(config) self.assertEqual(self.parser.values(), {'foo': {'bar': 1}, '__main__': {'baz': 0}}) op, options, args = schemaconfigglue(self.parser, argv=['--foo_bar', '2']) self.assertEqual(self.parser.values(), {'foo': {'bar': 2}, '__main__': {'baz': 0}}) def test_glue_missing_section(self): """Test schemaconfigglue with missing section.""" class MySchema(Schema): foo = DictOption() config = BytesIO(b"[__main__]\nfoo = bar") parser = SchemaConfigParser(MySchema()) parser.readfp(config) # hitting the parser directly raises an exception self.assertRaises(NoSectionError, parser.values) self.assertFalse(parser.is_valid()) # which is nicely handled by the glue code, so as not to crash it op, options, args = schemaconfigglue(parser) # there is no value for 'foo' due to the missing section self.assertEqual(options, {'foo': None}) def test_glue_json_dict(self): class MySchema(Schema): foo = DictOption() parser = SchemaConfigParser(MySchema()) op, options, args = schemaconfigglue(parser, argv=['--foo', '{"bar": "baz"}']) self.assertEqual(options, {'foo': '{"bar": "baz"}'}) self.assertEqual(parser.values(), {'__main__': {'foo': {'bar': 'baz'}}}) @patch('configglue.glue.os') def test_glue_environ(self, mock_os): mock_os.environ = {'CONFIGGLUE_FOO_BAR': '42', 'CONFIGGLUE_BAZ': 3} config = BytesIO(b"[foo]\nbar=1") self.parser.readfp(config) _argv, sys.argv = sys.argv, ['prognam'] try: op, options, args = schemaconfigglue(self.parser) self.assertEqual(self.parser.values(), {'foo': {'bar': 42}, '__main__': {'baz': 3}}) finally: sys.argv = _argv @patch('configglue.glue.os') def test_glue_environ_bad_name(self, mock_os): mock_os.environ = {'FOO_BAR': 2, 'BAZ': 3} config = BytesIO(b"[foo]\nbar=1") self.parser.readfp(config) _argv, sys.argv = sys.argv, ['prognam'] try: op, options, args = schemaconfigglue(self.parser) self.assertEqual(self.parser.values(), {'foo': {'bar': 1}, '__main__': {'baz': 0}}) finally: sys.argv = _argv def test_glue_environ_precedence(self): with patch.object(os, 'environ', {'CONFIGGLUE_FOO_BAR': '42', 'BAR': '1'}): config = BytesIO(b"[foo]\nbar=$BAR") self.parser.readfp(config) _argv, sys.argv = sys.argv, ['prognam'] try: op, options, args = schemaconfigglue(self.parser) self.assertEqual(self.parser.get('foo', 'bar'), 42) finally: sys.argv = _argv def test_glue_environ_precedence_fatal_option(self): class MySchema(Schema): foo = IntOption(fatal=True) parser = SchemaConfigParser(MySchema()) with patch.object(os, 'environ', {'CONFIGGLUE_FOO': '42'}): _argv, sys.argv = sys.argv, ['prognam'] try: op, options, args = schemaconfigglue(parser) self.assertEqual(parser.get('__main__', 'foo'), 42) finally: sys.argv = _argv def test_glue_environ_precedence_null_option(self): class MySchema(Schema): foo = StringOption(null=True) parser = SchemaConfigParser(MySchema()) with patch.object(os, 'environ', {'CONFIGGLUE_FOO': '42'}): _argv, sys.argv = sys.argv, ['prognam'] try: op, options, args = schemaconfigglue(parser) self.assertEqual(parser.get('__main__', 'foo'), '42') finally: sys.argv = _argv def test_glue_environ_precedence_null_and_fatal_option(self): class MySchema(Schema): foo = StringOption(null=True, fatal=True) parser = SchemaConfigParser(MySchema()) with patch.object(os, 'environ', {'CONFIGGLUE_FOO': '42'}): _argv, sys.argv = sys.argv, ['prognam'] try: op, options, args = schemaconfigglue(parser) self.assertEqual(parser.get('__main__', 'foo'), '42') finally: sys.argv = _argv def test_ambiguous_option(self): """Test schemaconfigglue when an ambiguous option is specified.""" class MySchema(Schema): class foo(Section): baz = IntOption() class bar(Section): baz = IntOption() config = BytesIO(b"[foo]\nbaz=1") parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertEqual(parser.values('foo'), {'baz': 1}) self.assertEqual(parser.values('bar'), {'baz': 0}) op, options, args = schemaconfigglue( parser, argv=['--bar_baz', '2']) self.assertEqual(parser.values('foo'), {'baz': 1}) self.assertEqual(parser.values('bar'), {'baz': 2}) def test_help(self): """Test schemaconfigglue with --help.""" config = BytesIO(b"[foo]\nbar=1") self.parser.readfp(config) self.assertEqual(self.parser.values(), {'foo': {'bar': 1}, '__main__': {'baz': 0}}) # replace stdout to capture its value new_callable = StringIO if PY2: new_callable = BytesIO with patch('sys.stdout', new_callable=new_callable) as mock_stdout: # call the method and assert its value self.assertRaises(SystemExit, schemaconfigglue, self.parser, argv=['--help']) # assert the value of stdout is correct output = mock_stdout.getvalue() self.assertTrue(output.startswith('Usage:')) def test_help_with_fatal(self): """Test schemaconfigglue with --help and an undefined fatal option.""" class MySchema(Schema): foo = IntOption(fatal=True) self.parser = SchemaConfigParser(MySchema()) # replace stdout to capture its value new_callable = StringIO if PY2: new_callable = BytesIO with patch('sys.stdout', new_callable=new_callable) as mock_stdout: # call the method and assert its value self.assertRaises(SystemExit, schemaconfigglue, self.parser, argv=['--help']) # assert the value of stdout is correct output = mock_stdout.getvalue() self.assertTrue(output.startswith('Usage:')) def test_parser_set_with_encoding(self): """Test schemaconfigglue override an option with a non-ascii value.""" class MySchema(Schema): foo = StringOption() parser = SchemaConfigParser(MySchema()) op, options, args = schemaconfigglue( parser, argv=['--foo', 'fóobâr']) self.assertEqual(parser.get('__main__', 'foo', parse=False), 'fóobâr') self.assertEqual(parser.get('__main__', 'foo'), 'fóobâr') def test_option_short_name(self): """Test schemaconfigglue support for short option names.""" class MySchema(Schema): foo = IntOption(short_name='f') parser = SchemaConfigParser(MySchema()) op, options, args = schemaconfigglue( parser, argv=['-f', '42']) self.assertEqual(parser.get('__main__', 'foo'), 42) def test_option_conflicting_short_name(self): """Test schemaconfigglue with conflicting short option names.""" class MySchema(Schema): foo = IntOption(short_name='f') flup = StringOption(short_name='f') parser = SchemaConfigParser(MySchema()) self.assertRaises(OptionConflictError, schemaconfigglue, parser, argv=['-f', '42']) def test_option_specified_twice(self): """Test schemaconfigglue with option name specified twice.""" class MySchema(Schema): foo = IntOption(short_name='f') parser = SchemaConfigParser(MySchema()) op, options, args = schemaconfigglue( parser, argv=['-f', '42', '--foo', '24']) self.assertEqual(parser.get('__main__', 'foo'), 24) op, options, args = schemaconfigglue( parser, argv=['-f', '24', '--foo', '42']) self.assertEqual(parser.get('__main__', 'foo'), 42) def test_fatal_option_with_config(self): class MySchema(Schema): foo = IntOption(fatal=True) config = BytesIO(b"[__main__]\nfoo=1") parser = SchemaConfigParser(MySchema()) parser.readfp(config) op, options, args = schemaconfigglue(parser) self.assertEqual(parser.values(), {'__main__': {'foo': 1}}) class ConfigglueTestCase(unittest.TestCase): @patch('configglue.glue.SchemaConfigParser') @patch('configglue.glue.schemaconfigglue') def test_configglue_no_errors(self, mock_schemaconfigglue, mock_schema_parser): """Test configglue when no errors occur.""" # prepare mocks expected_schema_parser = Mock() expected_schema_parser.is_valid.return_value = (True, None) expected_option_parser = Mock() expected_options = Mock() expected_args = Mock() mock_schemaconfigglue.return_value = (expected_option_parser, expected_options, expected_args) mock_schema_parser.return_value = expected_schema_parser # define the inputs class MySchema(Schema): foo = IntOption() configs = ['config.ini'] # call the function under test glue = configglue(MySchema, configs) # schema_parse is a SchemaConfigParser, initialized with MySchema # and fed with the configs file list self.assertEqual(glue.schema_parser, expected_schema_parser) mock_schema_parser.assert_called_with(MySchema()) mock_schema_parser.return_value.read.assert_called_with(configs) # the other attributes are the result of calling schemaconfigglue mock_schemaconfigglue.assert_called_with(expected_schema_parser, op=None) self.assertEqual(glue.option_parser, expected_option_parser) self.assertEqual(glue.options, expected_options) self.assertEqual(glue.args, expected_args) @patch('configglue.glue.SchemaConfigParser') @patch('configglue.glue.schemaconfigglue') def test_configglue_with_errors(self, mock_schemaconfigglue, mock_schema_parser): """Test configglue when an error happens.""" # prepare mocks expected_schema_parser = Mock() expected_schema_parser.is_valid.return_value = (False, ['some error']) expected_option_parser = Mock() expected_options = Mock() expected_args = Mock() mock_schemaconfigglue.return_value = (expected_option_parser, expected_options, expected_args) mock_schema_parser.return_value = expected_schema_parser # define the inputs class MySchema(Schema): foo = IntOption() configs = ['config.ini'] # call the function under test glue = configglue(MySchema, configs) # schema_parse is a SchemaConfigParser, initialized with MySchema # and fed with the configs file list self.assertEqual(glue.schema_parser, expected_schema_parser) mock_schema_parser.assert_called_with(MySchema()) mock_schema_parser.return_value.read.assert_called_with(configs) # the other attributes are the result of calling schemaconfigglue mock_schemaconfigglue.assert_called_with(expected_schema_parser, op=None) self.assertEqual(glue.option_parser, expected_option_parser) expected_option_parser.error.assert_called_with('some error') self.assertEqual(glue.options, expected_options) self.assertEqual(glue.args, expected_args) @patch('configglue.glue.SchemaConfigParser') @patch('configglue.glue.schemaconfigglue') def test_configglue_with_options(self, mock_schemaconfigglue, mock_schema_parser): """Test configglue with a custom OptionParser.""" # define the inputs class MySchema(Schema): foo = IntOption() configs = ['config.ini'] op = OptionParser(usage='foo') # prepare mocks expected_schema_parser = Mock() expected_schema_parser.is_valid.return_value = (True, None) expected_args = Mock() mock_schemaconfigglue.return_value = (op, op.values, expected_args) mock_schema_parser.return_value = expected_schema_parser # call the function under test glue = configglue(MySchema, configs, op=op) # schema_parse is a SchemaConfigParser, initialized with MySchema # and fed with the configs file list self.assertEqual(glue.schema_parser, expected_schema_parser) mock_schema_parser.assert_called_with(MySchema()) mock_schema_parser.return_value.read.assert_called_with(configs) # the other attributes are the result of calling schemaconfigglue mock_schemaconfigglue.assert_called_with(expected_schema_parser, op=op) self.assertEqual(glue.option_parser, op) self.assertEqual(glue.options, op.values) self.assertEqual(glue.args, expected_args) @patch('configglue.parser.SchemaConfigParser.is_valid') def test_configglue_no_validate(self, mock_is_valid): """Test configglue with validation disabled.""" mock_is_valid.return_value = (True, []) configglue(Schema, [], validate=False) # validation was not invoked self.assertEqual(mock_is_valid.called, False) @patch('configglue.parser.SchemaConfigParser.is_valid') def test_configglue_validate(self, mock_is_valid): """Test configglue with validation enabled.""" mock_is_valid.return_value = (True, []) configglue(Schema, [], validate=True) # validation was invoked self.assertEqual(mock_is_valid.called, True) @patch('configglue.parser.SchemaConfigParser.is_valid') def test_configglue_validate_default_value(self, mock_is_valid): """Test configglue validation default.""" mock_is_valid.return_value = (True, []) configglue(Schema, []) # validation was not invoked self.assertEqual(mock_is_valid.called, False) @patch('configglue.parser.SchemaConfigParser.is_valid') def test_configglue_validate_from_options(self, mock_is_valid): """Test configglue with validation from options.""" mock_is_valid.return_value = (True, []) op = OptionParser() op.add_option('--validate', dest='validate', action='store_true') with patch.object(sys, 'argv', ['foo', '--validate']): configglue(Schema, [], op=op) self.assertEqual(mock_is_valid.called, True) @patch('configglue.parser.SchemaConfigParser.is_valid') def test_configglue_validate_without_option(self, mock_is_valid): """Test configglue with validation from options.""" mock_is_valid.return_value = (True, []) op = OptionParser() with patch.object(sys, 'argv', ['foo']): configglue(Schema, [], op=op) self.assertEqual(mock_is_valid.called, False) configglue-1.1.2/configglue/tests/inischema/0000755000175000017500000000000012167605161022156 5ustar ricardoricardo00000000000000configglue-1.1.2/configglue/tests/inischema/__init__.py0000644000175000017500000000223412167556217024277 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### """Tests! Who woulda said""" # Two use cases so far for this file: # * make tests a package, so setup.py's "test" command finds the tests # * load all the tests if __name__ == '__main__': import unittest from configglue.tests import (test_attributed, test_typed, test_parsers, test_glue, test_glue2glue) suite = unittest.TestSuite() loader = unittest.TestLoader() for module in (test_attributed, test_typed, test_parsers, test_glue, test_glue2glue): suite.addTest(loader.loadTestsFromModule(module)) unittest.TextTestRunner(verbosity=2).run(suite) configglue-1.1.2/configglue/tests/inischema/test_glue.py0000644000175000017500000001041112167556226024527 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from __future__ import unicode_literals # in testfiles, putting docstrings on methods messes up with the # runner's output, so pylint: disable-msg=C0111 import sys import unittest from io import BytesIO, StringIO, TextIOWrapper from mock import patch from configglue._compat import PY2 from configglue.inischema.glue import configglue class TestBase(unittest.TestCase): """ Base class to keep common set-up """ def setUp(self): self.file = TextIOWrapper(BytesIO(self.ini)) self.old_sys_argv = sys.argv sys.argv = [''] def tearDown(self): sys.argv = self.old_sys_argv class TestGlue(TestBase): ini = b''' [blah] foo.help = yadda yadda yadda yadda foo.metavar = FOO foo.parser = int foo = 2 ''' arg = '--blah_foo' opt = 'blah_foo' val = 2 def test_ini_file_wins_when_no_args(self): parser, options, args = configglue(self.file, args=[]) self.assertEqual(vars(options), {self.opt: self.val}) def test_args_win(self): parser, options, args = configglue(self.file, args=['', self.arg + '=5']) self.assertEqual(vars(options), {self.opt: '5'}) def test_help_is_displayed(self): new_callable = StringIO if PY2: new_callable = BytesIO with patch('sys.stdout', new_callable=new_callable) as mock_stdout: try: configglue(self.file, args=['', '--help']) except SystemExit: output = mock_stdout.getvalue() self.assertTrue('yadda yadda yadda yadda' in output) class TestCrazyGlue(TestGlue): ini = b''' [bl-ah] foo.default = 3 foo.help = yadda yadda yadda yadda foo.metavar = FOO foo.parser = int foo = 2 ''' arg = '--bl-ah_foo' opt = 'bl_ah_foo' class TestNoValue(TestGlue): ini = b''' [blah] foo.help = yadda yadda yadda yadda foo.metavar = FOO foo.parser = int foo = 3 ''' val = 3 class TestGlue2(TestBase): ini = b'[__main__]\na=1\n' def test_main(self): parser, options, args = configglue(self.file) self.assertEqual(options.a, '1') class TestGlue3(TestBase): ini = b'[x]\na.help=hi\n' def test_empty(self): parser, options, args = configglue(self.file) self.assertEqual(options.x_a, '') def test_accepts_args_and_filenames(self): parser, options, args = configglue(self.file, 'dummy', args=['', '--x_a=1']) self.assertEqual(options.x_a, '1') class TestGlueBool(TestBase): ini = b'''[__main__] foo.parser=bool foo.action=store_true bar.default = True bar.parser = bool bar.action = store_false ''' def test_store_true(self): parser, options, args = configglue(self.file, args=['', '--foo']) self.assertEqual(options.foo, True) def test_store_false(self): parser, options, args = configglue(self.file, args=['', '--bar']) self.assertEqual(options.bar, False) class TestGlueLines(TestBase): ini = b'''[__main__] foo.parser = lines foo.action = append bar = a b bar.parser = lines bar.action = append ''' def test_nothing(self): parser, options, args = configglue(self.file) self.assertEqual(options.foo, []) def test_no_append(self): parser, options, args = configglue(self.file) self.assertEqual(options.bar, ['a', 'b']) def test_append_on_empty(self): parser, options, args = configglue(self.file, args=['', '--foo=x']) self.assertEqual(options.foo, ['x']) def test_append(self): parser, options, args = configglue(self.file, args=['', '--bar=x']) self.assertEqual(options.bar, ['a', 'b', 'x']) configglue-1.1.2/configglue/tests/inischema/test_glue2glue.py0000644000175000017500000000541512167556224025474 0ustar ricardoricardo00000000000000# -*- coding: utf-8 -*- ############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from __future__ import unicode_literals import sys import textwrap import unittest from io import StringIO from configglue.inischema.glue import ( configglue, ini2schema, ) from configglue.glue import schemaconfigglue class TestGlueConvertor(unittest.TestCase): def setUp(self): # make sure we have a clean sys.argv so as not to have unexpected test # results self.old_argv = sys.argv sys.argv = [] def tearDown(self): # restore old sys.argv sys.argv = self.old_argv def test_empty(self): s = "" _, cg, _ = configglue(StringIO(s)) _, sg, _ = schemaconfigglue(ini2schema(StringIO(s))) self.assertEqual(vars(cg), vars(sg)) def test_simple(self): s = "[foo]\nbar = 42\n" _, cg, _ = configglue(StringIO(s)) _, sg, _ = schemaconfigglue(ini2schema(StringIO(s))) self.assertEqual(vars(cg), vars(sg)) def test_main(self): s = "[__main__]\nbar = 42\n" _, cg, _ = configglue(StringIO(s)) _, sg, _ = schemaconfigglue(ini2schema(StringIO(s))) self.assertEqual(vars(cg), vars(sg)) def test_parser_none(self): s = "[__main__]\nbar = meeeeh\nbar.parser = none" _, cg, _ = configglue(StringIO(s), extra_parsers=[('none', str)]) _, sg, _ = schemaconfigglue(ini2schema(StringIO(s))) self.assertEqual(vars(cg), vars(sg)) def test_parser_unicode(self): s = textwrap.dedent(""" [__main__] bar = zátrapa """) _, cg, _ = configglue(StringIO(s)) _, sg, _ = schemaconfigglue(ini2schema(StringIO(s))) self.assertEqual(vars(cg), vars(sg)) def test_parser_int(self): s = "[__main__]\nbar = 42\nbar.parser = int\n" _, cg, _ = configglue(StringIO(s)) _, sg, _ = schemaconfigglue(ini2schema(StringIO(s))) self.assertEqual(vars(cg), vars(sg)) def test_parser_bool(self): s = "[__main__]\nbar = true\nbar.parser = bool \n" _, cg, _ = configglue(StringIO(s)) _, sg, _ = schemaconfigglue(ini2schema(StringIO(s))) self.assertEqual(vars(cg), vars(sg)) if __name__ == '__main__': unittest.main() configglue-1.1.2/configglue/tests/inischema/test_parsers.py0000644000175000017500000000307112167556231025252 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### # in testfiles, putting docstrings on methods messes up with the # runner's output, so pylint: disable-msg=C0111 import unittest from configglue.inischema import parsers class TestParsers(unittest.TestCase): def test_bool(self): for value in ('true', '1', 'on', 'yes', 'True', 'YES', 'oN'): self.assertEqual(parsers.bool_parser(value), True) for value in ('false', '0', 'off', 'no', 'faLSE', 'No', 'oFf'): self.assertEqual(parsers.bool_parser(value), False) for value in ('xyzzy', '', '-1', '0.', '0.1'): self.assertRaises(ValueError, parsers.bool_parser, value) def test_bool_not_string(self): self.assertEqual(parsers.bool_parser(1), True) def test_bool_is_None(self): self.assertEqual(parsers.bool_parser(None), False) def test_lines(self): self.assertEqual(parsers.lines('abc\ndef'), ['abc', 'def']) def test_lines_not_string(self): self.assertEqual(parsers.lines(42), 42) configglue-1.1.2/configglue/tests/inischema/test_typed.py0000644000175000017500000001145412167562305024723 0ustar ricardoricardo00000000000000# -*- encoding: utf-8 -*- ############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from __future__ import unicode_literals # in testfiles, putting docstrings on methods messes up with the # runner's output, so pylint: disable-msg=C0111 import unittest from io import StringIO from configglue._compat import RawConfigParser from configglue.inischema.typed import TypedConfigParser marker = object() def some_parser(value): if value == 'marker': return marker else: return None class BaseTest(unittest.TestCase): """ Base class to keep common set-up """ def setUp(self): self.config_string = ''' [xyzzy] foo.parser = complex foo.default = 1j bar.parser = int bar.default = -1 bar = 2 baz.parser = some.parser baz = marker baz2.parser = more.parser baz2 = -1 meep = árbol meep.parser = unicode thud.help = this is the help for thud woof.default = marker woof.default.parser = some.parser woof.parser = bool ''' self.config = TypedConfigParser() self.config.readfp(StringIO(self.config_string)) class TestBackwardsCompat(BaseTest): """ rather basic backwards compatibility checker """ def test_config_before_parse_is_plain(self): rawConfig = RawConfigParser() rawConfig.readfp(StringIO(self.config_string)) self.assertEqual([(section, sorted(self.config.items(section))) for section in self.config.sections()], [(section, sorted(rawConfig.items(section))) for section in rawConfig.sections()]) class TestParserd(BaseTest): """Test the different parsing situations""" def test_some_builtin_parser(self): self.config.parse('xyzzy', 'bar') self.assertEqual(self.config.get('xyzzy', 'bar').value, 2) def test_add_second_custom_parser_fails(self): self.config.add_parser('some.parser', some_parser) self.assertRaises(ValueError, self.config.add_parser, 'some.parser', some_parser) def test_custom_parser(self): self.config.add_parser('some.parser', some_parser) self.config.parse('xyzzy', 'baz') self.assertEqual(self.config.get('xyzzy', 'baz').value, marker) def test_value_is_default_if_empty(self): self.config.parse('xyzzy', 'foo') self.assertEqual(self.config.get('xyzzy', 'foo').value, 1j) def test_parse_default_parser(self): self.config.add_parser('some.parser', some_parser) self.config.parse('xyzzy', 'woof') self.assertTrue(self.config.get('xyzzy', 'woof').value) def test_parse_all_parses_all(self): self.config.add_parser('some.parser', some_parser) self.config.add_parser('more.parser', some_parser) self.config.parse_all() self.assertEqual([(section, [(k, v.value) for (k, v) in sorted(self.config.items(section))]) for section in self.config.sections()], [('xyzzy', [('bar', 2), ('baz', marker), ('baz2', None), ('foo', 1j), ('meep', '\xe1rbol'), ('thud', None), ('woof', True), ])]) def test_add_multiple_parsers(self): self.config.add_parsers(('some.parser', some_parser), ('more.parser', some_parser)) self.config.parse('xyzzy', 'baz') self.config.parse('xyzzy', 'baz2') self.assertEqual(self.config.get('xyzzy', 'baz').value, marker) self.assertEqual(self.config.get('xyzzy', 'baz2').value, None) def test_add_mutliple_with_repeat_without_clobber(self): self.assertRaises(ValueError, self.config.add_parsers, ('some.parser', some_parser), ('some.parser', some_parser)) def test_add_multiple_with_repeat_with_clobber(self): self.config.add_parsers(('some.parser', some_parser), ('some.parser', bool, True)) self.config.parse('xyzzy', 'baz') self.assertEqual(self.config.get('xyzzy', 'baz').value, True) if __name__ == '__main__': unittest.main() configglue-1.1.2/configglue/tests/inischema/test_attributed.py0000644000175000017500000000406212167562311025737 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from __future__ import unicode_literals # in testfiles, putting docstrings on methods messes up with the # runner's output, so pylint: disable-msg=C0111 import unittest from io import StringIO from configglue._compat import RawConfigParser from configglue.inischema.attributed import AttributedConfigParser class BaseTest(unittest.TestCase): """ Base class to keep common set-up """ def setUp(self): self.config_string = ''' [xyzzy] foo = 5 foo.banana = yellow foo.mango = orange foo.noise = white bar.blah = 23 ''' self.config = AttributedConfigParser() self.config.readfp(StringIO(self.config_string)) class TestAttributed(BaseTest): """ pretty basic tests of AttributedConfigParser """ def test_config_before_parsing_is_plain(self): rawConfig = RawConfigParser() rawConfig.readfp(StringIO(self.config_string)) self.assertEqual([(section, sorted(self.config.items(section))) for section in self.config.sections()], [(section, sorted(rawConfig.items(section))) for section in rawConfig.sections()]) def test_config_after_parsing_is_attributed(self): self.config.parse_all() self.assertEqual(self.config.get('xyzzy', 'foo').attrs['noise'], 'white') def test_config_after_parsing_still_knows_about_empty_values(self): self.config.parse_all() self.assertTrue(self.config.get('xyzzy', 'bar').is_empty) configglue-1.1.2/configglue/tests/app/0000755000175000017500000000000012167605161020776 5ustar ricardoricardo00000000000000configglue-1.1.2/configglue/tests/app/__init__.py0000644000175000017500000000000012147251621023071 0ustar ricardoricardo00000000000000configglue-1.1.2/configglue/tests/app/test_base.py0000644000175000017500000001415512167556201023327 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### import os from optparse import OptionParser from unittest import TestCase from mock import ( Mock, patch, ) from configglue.app.base import ( App, Config, ) from configglue.app.plugin import ( Plugin, PluginManager, ) from configglue.schema import ( IntOption, Schema, ) def make_app(name=None, schema=None, plugin_manager=None, validate=False, parser=None): # patch sys.argv so that nose can be run with extra options # without conflicting with the schema validation # patch sys.stderr to prevent spurious output mock_sys = Mock() mock_sys.argv = ['foo.py'] if validate: mock_sys.argv.append('--validate') with patch('configglue.glue.sys', mock_sys): with patch('configglue.app.base.sys.stderr'): app = App(name=name, schema=schema, plugin_manager=plugin_manager, parser=parser) return app def make_config(app=None): if app is None: app = make_app() # patch sys.argv so that nose can be run with extra options # without conflicting with the schema validation mock_sys = Mock() mock_sys.argv = ['foo.py'] with patch('configglue.glue.sys', mock_sys): config = Config(app) return config class ConfigTestCase(TestCase): def get_xdg_config_dirs(self): xdg_config_home = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config')) xdg_config_dirs = ([xdg_config_home] + os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg').split(':')) return xdg_config_dirs @patch('configglue.app.base.merge') @patch('configglue.app.base.Config.get_config_files') @patch('configglue.app.base.configglue') def test_constructor(self, mock_configglue, mock_get_config_files, mock_merge): app = App() config = Config(app) self.assertEqual(config.schema, mock_merge.return_value) self.assertEqual(config.glue, mock_configglue.return_value) mock_configglue.assert_called_with( mock_merge.return_value, mock_get_config_files.return_value, op=app.parser) def test_glue_valid_config(self): config = make_config() self.assertEqual(config.glue.schema_parser.is_valid(), True) def test_glue_validate_invalid_config(self): class MySchema(Schema): foo = IntOption(fatal=True) self.assertRaises(SystemExit, make_app, schema=MySchema, validate=True) def test_glue_no_validate_invalid_config(self): class MySchema(Schema): foo = IntOption(fatal=True) # no explicit assertion as we just want to verify creating the # app doesn't raise any exception if validation is turned off make_app(schema=MySchema) def test_get_config_files(self): app = make_app() config = make_config(app=app) self.assertEqual(config.get_config_files(app), []) @patch('xdg.BaseDirectory.os.path.exists') def test_get_config_files_full_hierarchy(self, mock_path_exists): mock_path_exists.return_value = True config_files = [] for path in reversed(self.get_xdg_config_dirs()): config_files.append(os.path.join(path, 'myapp', 'myapp.cfg')) config_files.append('./local.cfg') app = make_app(name='myapp') config = make_config(app=app) self.assertEqual(config.get_config_files(app=app), config_files) @patch('xdg.BaseDirectory.os.path.exists') def test_get_config_files_with_plugins_full_hierarchy(self, mock_path_exists): mock_path_exists.return_value = True class Foo(Plugin): enabled = True config_files = [] for path in reversed(self.get_xdg_config_dirs()): config_files.append(os.path.join(path, 'myapp', 'myapp.cfg')) config_files.append(os.path.join(path, 'myapp', 'foo.cfg')) config_files.append('./local.cfg') app = make_app(name='myapp') app.plugins.register(Foo) config = make_config(app=app) self.assertEqual(config.get_config_files(app=app), config_files) class AppTestCase(TestCase): def test_custom_name(self): app = make_app(name='myapp') self.assertEqual(app.name, 'myapp') @patch('configglue.app.base.sys') def test_default_name(self, mock_sys): mock_sys.argv = ['foo.py'] app = make_app() self.assertEqual(app.name, 'foo') def test_default_plugin_manager(self): app = make_app() self.assertEqual(type(app.plugins), PluginManager) def test_custom_plugin_manager(self): mock_plugin_manager = Mock() mock_plugin_manager.schemas = [] app = make_app(plugin_manager=mock_plugin_manager) self.assertEqual(app.plugins, mock_plugin_manager) @patch('configglue.app.base.Config') def test_config(self, mock_config): app = make_app() self.assertEqual(app.config, mock_config.return_value) def test_default_parser(self): app = make_app() # parser is configured self.assertNotEqual(app.parser, None) self.assertEqual(app.config.glue.option_parser, app.parser) # there is only one option by default: --validate self.assertEqual(app.parser.values.__dict__, {'validate': False}) def test_custom_parser(self): custom_parser = OptionParser() custom_parser.add_option('-f', '--foo') app = make_app(parser=custom_parser) # parser is configured self.assertEqual(app.parser, custom_parser) self.assertEqual(app.config.glue.option_parser, custom_parser) configglue-1.1.2/configglue/tests/app/test_plugin.py0000644000175000017500000000516012167556203023711 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from unittest import TestCase from configglue.app.plugin import ( Plugin, PluginManager, ) from configglue.schema import Schema def make_plugins(available=None, enabled=None): plugins = PluginManager() if available: for plugin in available: plugins.register(plugin) if enabled is not None: for plugin in enabled: plugins.enable(plugin) return plugins class Foo(Plugin): pass class PluginTestCase(TestCase): def test_defaults(self): plugin = Plugin() self.assertEqual(plugin.schema, Schema) self.assertEqual(plugin.enabled, False) class PluginManagerTestCase(TestCase): def test_constructor(self): plugins = make_plugins() self.assertEqual(plugins.available, []) def test_enabled(self): plugins = make_plugins(available=[Foo], enabled=[Foo]) self.assertEqual(plugins.enabled, [Foo]) def test_enable(self): plugins = make_plugins(available=[Foo]) self.assertEqual(Foo.enabled, False) self.assertTrue(Foo in plugins.available) self.assertFalse(Foo in plugins.enabled) plugins.enable(Foo) self.assertEqual(Foo.enabled, True) self.assertTrue(Foo in plugins.enabled) def test_disable(self): plugins = make_plugins(available=[Foo], enabled=[Foo]) self.assertEqual(Foo.enabled, True) self.assertTrue(Foo in plugins.enabled) self.assertTrue(Foo in plugins.enabled) plugins.disable(Foo) self.assertEqual(Foo.enabled, False) self.assertFalse(Foo in plugins.enabled) def test_schemas(self): class Bar(Plugin): pass plugins = make_plugins(available=[Foo, Bar], enabled=[Foo]) self.assertEqual(plugins.schemas, [Foo.schema]) def test_load(self): plugins = make_plugins() self.assertEqual(plugins.load(), []) def test_register(self): plugins = PluginManager() self.assertEqual(plugins.available, []) plugins.register(Foo) self.assertEqual(plugins.available, [Foo]) configglue-1.1.2/configglue/tests/test_schema.py0000644000175000017500000012555212167557144023110 0ustar ricardoricardo00000000000000# -*- coding: utf-8 -*- ############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from __future__ import unicode_literals import textwrap import unittest from io import BytesIO from configglue._compat import text_type from configglue._compat import NoOptionError, NoSectionError from configglue.parser import ( SchemaConfigParser, SchemaValidationError, ) from configglue.schema import ( BoolOption, Option, Section, DictOption, IntOption, ListOption, Schema, StringOption, TupleOption, get_config_objects, merge, ) class TestSchema(unittest.TestCase): def test_sections(self): """Test Schema sections.""" class MySchema(Schema): foo = BoolOption() class MyOtherSchema(Schema): class web(Section): bar = IntOption() class froo(Section): twaddle = ListOption(item=BoolOption()) class MyThirdSchema(Schema): bar = IntOption() class froo(Section): twaddle = ListOption(item=BoolOption()) schema = MySchema() names = set(s.name for s in schema.sections()) self.assertEquals(set(['__main__']), names) schema = MyOtherSchema() names = set(s.name for s in schema.sections()) self.assertEquals(set(['web', 'froo']), names) schema = MyThirdSchema() names = set(s.name for s in schema.sections()) self.assertEquals(set(['__main__', 'froo']), names) def test_schema_validation(self): """Test Schema validation.""" class BorkenSchema(Schema): class __main__(Section): foo = BoolOption() class SomeSchema(Schema): class mysection(Section): pass schema = BorkenSchema() self.assertFalse(schema.is_valid()) schema = SomeSchema() self.assertTrue(schema.is_valid()) def test_names(self): """Test Schema section/option names.""" class MySchema(Schema): foo = BoolOption() class bar(Section): baz = IntOption() schema = MySchema() self.assertEquals('foo', schema.foo.name) self.assertEquals('__main__', schema.foo.section.name) self.assertEquals('bar', schema.bar.name) self.assertEquals('baz', schema.bar.baz.name) self.assertEquals('bar', schema.bar.baz.section.name) def test_options(self): """Test Schema options.""" class MySchema(Schema): foo = BoolOption() class bar(Section): baz = IntOption() schema = MySchema() names = set(s.name for s in schema.options()) self.assertEquals(set(['foo', 'baz']), names) names = set(s.name for s in schema.options('__main__')) self.assertEquals(set(['foo']), names) names = set(s.name for s in schema.options('bar')) self.assertEquals(set(['baz']), names) def test_include(self): schema = Schema() self.assertTrue(hasattr(schema, 'includes')) def test_equal(self): """Test Schema equality.""" class MySchema(Schema): foo = IntOption() class OtherSchema(Schema): bar = IntOption() self.assertEqual(MySchema(), MySchema()) self.assertNotEqual(MySchema(), OtherSchema()) class TestSchemaHelpers(unittest.TestCase): def test_get_config_objects(self): """Test get_config_objects.""" class MySchema(Schema): foo = IntOption() class one(Section): bar = IntOption() two = Section() two.bam = IntOption() expected = { 'foo': MySchema.foo, 'one': MySchema.one(), 'two': MySchema.two, } objects = dict(get_config_objects(MySchema)) self.assertEqual(objects.keys(), expected.keys()) # cannot compare for just equal as inner classes are # instantiated when get_config_objects is called for key, value in expected.items(): self.assertEqual(type(objects[key]), type(value)) class TestOption(unittest.TestCase): cls = Option def test_equal(self): """Test option equality.""" opt1 = IntOption(name='opt1') opt2 = IntOption(name='opt2') opt3 = StringOption(name='opt1') self.assertEqual(opt1, opt1) self.assertNotEqual(opt1, opt2) self.assertNotEqual(opt1, opt3) def test_equal_when_in_section(self): """Test option equality for section options.""" sect1 = Section(name='sect1') sect2 = Section(name='sect2') opt1 = IntOption() opt2 = IntOption() self.assertEqual(opt1, opt2) opt1.section = sect1 opt2.section = sect2 self.assertNotEqual(opt1, opt2) def test_equal_when_error(self): """Test option equality when errors.""" opt1 = IntOption() opt2 = IntOption() # make sure an attribute error is raised del opt2.name self.assertNotEqual(opt1, opt2) def test_validate(self): """Test Option default validate behaviour.""" opt = self.cls() self.assertRaises(NotImplementedError, opt.validate, 0) def test_short_name(self): """Test Option short name.""" opt = self.cls(short_name='f') self.assertEqual(opt.short_name, 'f') class TestSchemaInheritance(unittest.TestCase): def setUp(self): class SchemaA(Schema): class foo(Section): bar = IntOption() class SchemaB(SchemaA): class baz(Section): wham = IntOption() class SchemaC(SchemaA): class bar(Section): woof = IntOption() self.schema = SchemaB() self.other = SchemaC() def test_basic_inheritance(self): """Test basic schema inheritance.""" names = [('foo', ['bar']), ('baz', ['wham'])] for section, options in names: section_obj = getattr(self.schema, section) self.assertTrue(isinstance(section_obj, Section)) for option in options: option_obj = getattr(section_obj, option) self.assertTrue(isinstance(option_obj, IntOption)) def test_inherited_sections(self): names = set(s.name for s in self.schema.sections()) self.assertEqual(set(['foo', 'baz']), names) def test_inherited_options(self): names = set(s.name for s in self.schema.options()) self.assertEqual(set(['bar', 'wham']), names) def test_mutable_inherited(self): """Test modifying inherited attribute doesn't affect parent.""" # modify one inherited attribute self.schema.foo.baz = IntOption() # test on the other schema self.assertFalse(hasattr(self.other.foo, 'baz')) def test_merge_inherited(self): """Test inherited schema overrides attributes as expected.""" class SchemaA(Schema): class foo(Section): bar = IntOption() bar = IntOption() class SchemaB(SchemaA): class foo(SchemaA.foo): baz = IntOption() # SchemaB inherits attributes from SchemaA and merges its own # attributes into schema = SchemaB() section_names = set(s.name for s in schema.sections()) option_names = set(o.name for o in schema.options('__main__')) foo_option_names = set(o.name for o in schema.options('foo')) self.assertEqual(section_names, set(['__main__', 'foo'])) self.assertEqual(option_names, set(['bar'])) self.assertEqual(foo_option_names, set(['bar', 'baz'])) # SchemaB inheritance does not affect SchemaA schema = SchemaA() section_names = set(s.name for s in schema.sections()) option_names = set(o.name for o in schema.options('__main__')) foo_option_names = set(o.name for o in schema.options('foo')) self.assertEqual(section_names, set(['__main__', 'foo'])) self.assertEqual(option_names, set(['bar'])) self.assertEqual(foo_option_names, set(['bar'])) class TestStringOption(unittest.TestCase): cls = StringOption def setUp(self): self.opt = self.cls() def test_init_no_args(self): """Test null attribute for StringOption.""" self.assertFalse(self.opt.null) def test_init_null(self): """Test null attribute for null StringOption.""" opt = self.cls(null=True) self.assertTrue(opt.null) def test_parse_ascii_string(self): """Test StringOption parse an ascii string.""" value = self.opt.parse('42') self.assertEqual(value, '42') def test_parse_empty_string(self): """Test StringOption parse an empty string.""" value = self.opt.parse('') self.assertEqual(value, '') def test_parse_null_string(self): """Test StringOption parse a null string.""" opt = self.cls(null=True) value = opt.parse(None) self.assertEqual(value, None) def test_None_string(self): """Test StringOption parse the 'None' string.""" value = self.opt.parse('None') self.assertEqual(value, 'None') def test_parse_nonascii_string(self): """Test StringOption parse a non-ascii string.""" value = self.opt.parse('foóbâr') self.assertEqual(value, 'foóbâr') def test_parse_int(self): """Test StringOption parse an integer.""" value = self.opt.parse(42) self.assertEqual(value, '42') def test_parse_bool(self): """Test StringOption parse a boolean.""" value = self.opt.parse(False) self.assertEqual(value, 'False') def test_default(self): """Test default value for StringOption.""" self.assertEqual(self.opt.default, '') def test_default_null(self): """Test default value for null StringOption.""" opt = self.cls(null=True) self.assertEqual(opt.default, None) def test_validate_string(self): """Test OptionString validate a string value.""" self.assertEqual(self.opt.validate(''), True) def test_validate_nonstring(self): """Test OptionString validate a non-string value.""" self.assertEqual(self.opt.validate(0), False) def test_validate_null_string(self): """Test OptionString validate a null string.""" opt = StringOption(null=True) self.assertEqual(opt.validate(None), True) def test_to_string_for_null_string(self): """Test OptionString to_string for a null string.""" opt = StringOption(null=True) self.assertEqual(opt.to_string(None), 'None') self.assertEqual(opt.to_string(''), '') self.assertEqual(opt.to_string('foo'), 'foo') def test_to_string_for_non_null_string(self): """Test OptionString to_string for non-null string.""" self.assertEqual(self.opt.to_string(None), None) self.assertEqual(self.opt.to_string(''), '') self.assertEqual(self.opt.to_string('foo'), 'foo') def test_short_name(self): """Test StringOption short name.""" opt = self.cls(short_name='f') self.assertEqual(opt.short_name, 'f') def test_equal(self): """Test StringOption equality.""" option1 = StringOption() option2 = StringOption(null=True) self.assertEqual(option1, option1) self.assertNotEqual(option1, option2) class TestIntOption(unittest.TestCase): cls = IntOption def test_parse_int(self): """Test IntOption parse an integer.""" class MySchema(Schema): foo = self.cls() config = BytesIO(b"[__main__]\nfoo = 42") expected_values = {'__main__': {'foo': 42}} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEqual(parser.values(), expected_values) config = BytesIO(b"[__main__]\nfoo =") parser = SchemaConfigParser(schema) parser.readfp(config) self.assertRaises(ValueError, parser.values) config = BytesIO(b"[__main__]\nfoo = bla") parser = SchemaConfigParser(schema) parser.readfp(config) self.assertRaises(ValueError, parser.values) def test_default(self): """Test IntOption default value.""" opt = self.cls() self.assertEqual(opt.default, 0) def test_validate_int(self): """Test IntOption validate an integer value.""" opt = self.cls() self.assertEqual(opt.validate(0), True) def test_validate_nonint(self): """Test IntOption validate a non-integer value.""" opt = self.cls() self.assertEqual(opt.validate(''), False) def test_short_name(self): """Test IntOption short name.""" opt = self.cls(short_name='f') self.assertEqual(opt.short_name, 'f') class TestBoolOption(unittest.TestCase): cls = BoolOption def test_parse_bool(self): """Test BoolOption parse a boolean value.""" class MySchema(Schema): foo = self.cls() config = BytesIO(b"[__main__]\nfoo = Yes") expected_values = {'__main__': {'foo': True}} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEqual(parser.values(), expected_values) config = BytesIO(b"[__main__]\nfoo = tRuE") parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEqual(parser.values(), expected_values) config = BytesIO(b"[__main__]\nfoo =") parser = SchemaConfigParser(schema) parser.readfp(config) self.assertRaises(ValueError, parser.values) config = BytesIO(b"[__main__]\nfoo = bla") parser = SchemaConfigParser(schema) parser.readfp(config) self.assertRaises(ValueError, parser.values) def test_default(self): """Test BoolOption default value.""" opt = self.cls() self.assertEqual(opt.default, False) def test_validate_bool(self): """Test BoolOption validate a boolean value.""" opt = self.cls() self.assertEqual(opt.validate(False), True) def test_validate_nonbool(self): """Test BoolOption value a non-boolean value.""" opt = self.cls() self.assertEqual(opt.validate(''), False) def test_short_name(self): """Test BoolOption short name.""" opt = self.cls(short_name='f') self.assertEqual(opt.short_name, 'f') class TestListOption(unittest.TestCase): cls = ListOption def test_parse_int_lines(self): """Test ListOption parse a list of integers.""" class MySchema(Schema): foo = self.cls(item=IntOption()) config = BytesIO(b"[__main__]\nfoo = 42\n 43\n 44") expected_values = {'__main__': {'foo': [42, 43, 44]}} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEqual(parser.values(), expected_values) def test_parse_no_json(self): class MySchema(Schema): foo = self.cls(item=IntOption(), parse_json=False) config = BytesIO(b"[__main__]\nfoo = 42\n 43\n 44") expected_values = {'__main__': {'foo': [42, 43, 44]}} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEqual(parser.values(), expected_values) def test_parse_no_json_with_json(self): class MySchema(Schema): foo = self.cls(item=IntOption(), parse_json=False) config = BytesIO(b"[__main__]\nfoo = [42, 43, 44]") schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertRaises(ValueError, parser.values) def test_parse_json(self): class MySchema(Schema): foo = self.cls(item=IntOption()) config = BytesIO(b"[__main__]\nfoo = [42, 43, 44]") expected_values = {'__main__': {'foo': [42, 43, 44]}} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEqual(parser.values(), expected_values) def test_parse_invalid_json(self): class MySchema(Schema): foo = self.cls(item=IntOption()) config = BytesIO(b'[__main__]\nfoo = 1, 2, 3') schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertRaises(ValueError, parser.values) def test_parse_non_list_json(self): class MySchema(Schema): foo = self.cls(item=IntOption()) config = BytesIO(b'[__main__]\nfoo = {"foo": "bar"}') schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertRaises(ValueError, parser.values) def test_parse_bool_lines(self): """Test ListOption parse a list of booleans.""" class MySchema(Schema): foo = self.cls(item=BoolOption()) schema = MySchema() config = BytesIO(b"[__main__]\nfoo = tRuE\n No\n 0\n 1") expected_values = {'__main__': {'foo': [True, False, False, True]}} parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEqual(expected_values, parser.values()) def test_parse_bool_empty_lines(self): """Test ListOption parse an empty list of booleans.""" class MySchema(Schema): foo = self.cls(item=BoolOption()) schema = MySchema() config = BytesIO(b"[__main__]\nfoo =") parser = SchemaConfigParser(schema) parser.readfp(config) expected_values = {'__main__': {'foo': []}} self.assertEqual(expected_values, parser.values()) def test_parse_bool_invalid_lines(self): """Test ListOption parse an invalid list of booleans.""" class MySchema(Schema): foo = self.cls(item=BoolOption()) schema = MySchema() config = BytesIO(b"[__main__]\nfoo = bla") parser = SchemaConfigParser(schema) parser.readfp(config) self.assertRaises(ValueError, parser.values) config = BytesIO(b"[__main__]\nfoo = True\n bla") parser = SchemaConfigParser(schema) parser.readfp(config) self.assertRaises(ValueError, parser.values) def test_default(self): """Test ListOption default value.""" opt = self.cls(item=IntOption()) self.assertEqual(opt.default, []) def test_remove_duplicates(self): """Test ListOption with remove_duplicates.""" class MySchema(Schema): foo = self.cls(item=StringOption(), remove_duplicates=True) schema = MySchema() config = BytesIO(b"[__main__]\nfoo = bla\n blah\n bla") parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEquals({'__main__': {'foo': ['bla', 'blah']}}, parser.values()) def test_remove_dict_duplicates(self): """Test ListOption remove_duplicates with DictOption.""" class MyOtherSchema(Schema): foo = self.cls(item=DictOption(), remove_duplicates=True) schema = MyOtherSchema() config = BytesIO(b"[__main__]\nfoo = bla\n bla\n[bla]\nbar = baz") parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEquals({'__main__': {'foo': [{'bar': 'baz'}]}}, parser.values()) def test_validate_list(self): """Test ListOption validate a list value.""" opt = self.cls(item=IntOption()) self.assertEqual(opt.validate([]), True) def test_validate_nonlist(self): """Test ListOption validate a non-list value.""" opt = self.cls(item=IntOption()) self.assertEqual(opt.validate(''), False) def test_default_item(self): """Test ListOption default item.""" opt = self.cls() self.assertEqual(opt.item, StringOption()) def test_short_name(self): """Test ListOption short name.""" opt = self.cls(short_name='f') self.assertEqual(opt.short_name, 'f') def test_equal(self): """Test ListOption equality.""" option1 = ListOption() option2 = ListOption(name='foo') option3 = ListOption(item=StringOption()) option4 = ListOption(item=DictOption()) option5 = ListOption(raw=True) option6 = ListOption(remove_duplicates=True) option7 = ListOption(raw=False, item=StringOption(raw=True)) self.assertEqual(option1, option1) self.assertNotEqual(option1, option2) self.assertEqual(option1, option3) self.assertNotEqual(option1, option4) self.assertNotEqual(option1, option5) self.assertNotEqual(option1, option6) self.assertNotEqual(option1, option7) def test_to_string_when_json(self): option = ListOption() result = option.to_string(['1', '2', '3']) self.assertEqual(result, '["1", "2", "3"]') self.assertNotEqual(result, "['1', '2', '3']") def test_to_string_when_no_json(self): option = ListOption(parse_json=False) result = option.to_string(['1', '2', '3']) expected = [text_type(x) for x in [1, 2, 3]] self.assertEqual(result, text_type(expected)) class TestTupleOption(unittest.TestCase): cls = TupleOption def test_init(self): """Test TupleOption constructor.""" opt = self.cls(length=2) self.assertEqual(opt.length, 2) def test_init_no_length(self): """Test TupleOption default attribute values.""" opt = self.cls() self.assertEqual(opt.length, 0) self.assertEqual(opt.default, ()) def test_parse_no_length(self): """Test TupleOption parse without length.""" class MySchema(Schema): foo = self.cls() config = BytesIO(b'[__main__]\nfoo=1,2,3,4') expected_values = {'__main__': {'foo': ('1', '2', '3', '4')}} parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertEqual(parser.values(), expected_values) def test_parse_tuple(self): """Test TupleOption parse with length.""" class MySchema(Schema): foo = self.cls(length=4) config = BytesIO(b'[__main__]\nfoo = 1, 2, 3, 4') expected_values = {'__main__': {'foo': ('1', '2', '3', '4')}} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEqual(parser.values(), expected_values) config = BytesIO(b'[__main__]\nfoo = 1, 2, 3') parser = SchemaConfigParser(schema) parser.readfp(config) self.assertRaises(ValueError, parser.values) config = BytesIO(b'[__main__]\nfoo = ') parser = SchemaConfigParser(schema) parser.readfp(config) self.assertRaises(ValueError, parser.values) def test_default(self): """Test TupleOption default value.""" opt = self.cls(length=2) self.assertEqual(opt.default, ()) def test_validate_tuple(self): """Test TupleOption validate a tuple value.""" opt = self.cls(length=2) self.assertEqual(opt.validate(()), True) def test_validate_nontuple(self): """Test TupleOption validate a non-tuple value.""" opt = self.cls(length=2) self.assertEqual(opt.validate(0), False) def test_short_name(self): """Test TupleOption short name.""" opt = self.cls(short_name='f') self.assertEqual(opt.short_name, 'f') def test_equal(self): """Test TupleOption equality.""" option1 = TupleOption() option2 = TupleOption(length=2) self.assertEqual(option1, option1) self.assertNotEqual(option1, option2) class TestDictOption(unittest.TestCase): cls = DictOption def test_init(self): """Test default values for DictOption attributes.""" opt = self.cls() self.assertEqual(opt.spec, {}) self.assertEqual(opt.strict, False) spec = {'a': IntOption(), 'b': BoolOption()} opt = self.cls(spec=spec) self.assertEqual(opt.spec, spec) self.assertEqual(opt.strict, False) opt = self.cls(spec=spec, strict=True) self.assertEqual(opt.spec, spec) self.assertEqual(opt.strict, True) def test_get_extra_sections(self): """Test DictOption get_extra_sections.""" class MySchema(Schema): foo = self.cls(item=self.cls()) config = BytesIO(b""" [__main__] foo=dict1 [dict1] bar=dict2 [dict2] baz=42 """) parser = SchemaConfigParser(MySchema()) parser.readfp(config) expected = ['dict2'] opt = self.cls(item=self.cls()) extra = opt.get_extra_sections('dict1', parser) self.assertEqual(extra, expected) def test_parse_dict(self): """Test DictOption parse a dict.""" class MySchema(Schema): foo = self.cls(spec={ 'bar': StringOption(), 'baz': IntOption(), 'bla': BoolOption(), }) config = BytesIO(b"""[__main__] foo = mydict [mydict] bar=baz baz=42 bla=Yes """) expected_values = { '__main__': { 'foo': {'bar': 'baz', 'baz': 42, 'bla': True}}} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEqual(parser.values(), expected_values) def test_parse_dict_no_json(self): """Test DictOption parse a dict when json is disabled.""" class MySchema(Schema): foo = self.cls(spec={ 'bar': StringOption(), 'baz': IntOption(), 'bla': BoolOption(), }, parse_json=False) config = BytesIO(b"""[__main__] foo = mydict [mydict] bar=baz baz=42 bla=Yes """) expected_values = { '__main__': { 'foo': {'bar': 'baz', 'baz': 42, 'bla': True}}} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEqual(parser.values(), expected_values) def test_parse_dict_json(self): """Test DictOption parse a json dict.""" class MySchema(Schema): foo = self.cls(spec={ 'bar': StringOption(), 'baz': IntOption(), 'bla': BoolOption(), }) config = BytesIO(textwrap.dedent(""" [__main__] foo = { "bar": "baz", "baz": "42", "bla": "Yes"} """).encode('utf-8')) expected_values = { '__main__': { 'foo': {'bar': 'baz', 'baz': 42, 'bla': True}}} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEqual(parser.values(), expected_values) def test_parse_dict_json_invalid_json(self): """Test DictOption parse invalid json.""" class MySchema(Schema): foo = self.cls(spec={ 'bar': StringOption(), 'baz': IntOption(), 'bla': BoolOption(), }) config = BytesIO(textwrap.dedent(""" [__main__] foo = {'bar': 23} """).encode('utf-8')) schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertRaises(NoSectionError, parser.values) def test_parse_dict_json_non_dict_json(self): """Test DictOption parse json not representing a dict.""" class MySchema(Schema): foo = self.cls(spec={ 'bar': StringOption(), 'baz': IntOption(), 'bla': BoolOption(), }) config = BytesIO(textwrap.dedent(""" [__main__] foo = [1, 2, 3] """).encode('utf-8')) schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertRaises(NoSectionError, parser.values) def test_parse_dict_no_json_with_json(self): """Test DictOption parse json when json is disabled.""" class MySchema(Schema): foo = self.cls(spec={ 'bar': StringOption(), 'baz': IntOption(), 'bla': BoolOption(), }, parse_json=False) config = BytesIO(textwrap.dedent(""" [__main__] foo = { "bar": "baz", "baz": "42", "bla": "Yes"} """).encode('utf-8')) schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertRaises(NoSectionError, parser.values) def test_parse_raw(self): """Test DictOption parse using raw=True.""" class MySchema(Schema): foo = self.cls(spec={ 'bar': StringOption(), 'baz': IntOption(), 'bla': BoolOption(), }) config = BytesIO(b"""[__main__] foo = mydict [mydict] baz=42 """) expected = {'bar': '', 'baz': '42', 'bla': 'False'} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) parsed = schema.foo.parse('mydict', parser, True) self.assertEqual(parsed, expected) def test_parse_json_raw_with_interpolation_marks(self): """Test DictOption parse json using raw=True when data has interpolation marks.""" class MySchema(Schema): class logging(Section): formatters = self.cls(raw=True, item=self.cls()) config = BytesIO(textwrap.dedent(""" [logging] formatters = {"sample": {"format": "%(name)s"}} """).encode('utf-8')) expected = {'sample': {'format': '%(name)s'}} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) parsed = parser.values('logging')['formatters'] self.assertEqual(parsed, expected) def test_parse_no_json_raw_with_interpolation_marks(self): """Test DictOption parse non-json using raw=True when data has interpolation marks.""" class MySchema(Schema): class logging(Section): formatters = self.cls(raw=True, item=self.cls()) config = BytesIO(textwrap.dedent(""" [logging] formatters = logging_formatters [logging_formatters] sample = sample_formatter [sample_formatter] format = %%(name)s """).encode('utf-8')) expected = {'sample': {'format': '%(name)s'}} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) parsed = parser.values('logging')['formatters'] self.assertEqual(parsed, expected) def test_parse_invalid_key_in_parsed(self): """Test DictOption parse with an invalid key in the config.""" class MySchema(Schema): foo = self.cls(spec={'bar': IntOption()}) config = BytesIO(b"[__main__]\nfoo=mydict\n[mydict]\nbaz=2") expected_values = {'__main__': {'foo': {'bar': 0, 'baz': '2'}}} parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertEqual(parser.values(), expected_values) def test_parse_invalid_key_in_spec(self): """Test DictOption parse with an invalid key in the spec.""" class MySchema(Schema): foo = self.cls(spec={ 'bar': IntOption(), 'baz': IntOption(fatal=True)}) config = BytesIO(b"[__main__]\nfoo=mydict\n[mydict]\nbar=2") parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertRaises(ValueError, parser.parse_all) def test_default(self): opt = self.cls(spec={}) self.assertEqual(opt.default, {}) def test_parse_no_strict_missing_args(self): """Test DictOption parse a missing key in non-strict mode.""" class MySchema(Schema): foo = self.cls(spec={'bar': IntOption()}) config = BytesIO(b"[__main__]\nfoo=mydict\n[mydict]") expected_values = {'__main__': {'foo': {'bar': 0}}} parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertEqual(parser.values(), expected_values) def test_parse_no_strict_extra_args(self): class MySchema(Schema): foo = self.cls() config = BytesIO(b"[__main__]\nfoo=mydict\n[mydict]\nbar=2") expected_values = {'__main__': {'foo': {'bar': '2'}}} parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertEqual(parser.values(), expected_values) def test_parse_no_strict_with_item(self): """Test DictOption parse in non-strict mode with an item spec.""" class MySchema(Schema): foo = self.cls( item=self.cls( item=IntOption())) config = BytesIO(b""" [__main__] foo = mydict [mydict] bar = baz [baz] wham=42 """) expected_values = {'__main__': {'foo': {'bar': {'wham': 42}}}} parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertEqual(parser.values(), expected_values) def test_parse_strict(self): """Test DictOption parse in strict mode.""" class MySchema(Schema): spec = {'bar': IntOption()} foo = self.cls(spec=spec, strict=True) config = BytesIO(b"[__main__]\nfoo=mydict\n[mydict]\nbar=2") expected_values = {'__main__': {'foo': {'bar': 2}}} parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertEqual(parser.values(), expected_values) def test_parse_strict_missing_vars(self): """Test DictOption parse in strict mode with missing values.""" class MySchema(Schema): spec = {'bar': IntOption(), 'baz': IntOption()} foo = self.cls(spec=spec, strict=True) config = BytesIO(b"[__main__]\nfoo=mydict\n[mydict]\nbar=2") expected_values = {'__main__': {'foo': {'bar': 2, 'baz': 0}}} parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertEqual(parser.values(), expected_values) def test_parse_strict_extra_vars(self): """Test DictOption parse in strict mode with extra values.""" class MySchema(Schema): spec = {'bar': IntOption()} foo = self.cls(spec=spec, strict=True) config = BytesIO(b"[__main__]\nfoo=mydict\n[mydict]\nbar=2\nbaz=3") parser = SchemaConfigParser(MySchema()) parser.readfp(config) self.assertRaises(ValueError, parser.parse_all) def test_validate_dict(self): opt = self.cls() self.assertEqual(opt.validate({}), True) def test_validate_nondict(self): opt = self.cls() self.assertEqual(opt.validate(0), False) def test_short_name(self): """Test DictOption short name.""" opt = self.cls(short_name='f') self.assertEqual(opt.short_name, 'f') def test_equal(self): """Test DictOption equality.""" option1 = DictOption() option2 = DictOption(spec={'foo': BoolOption()}) option3 = DictOption(strict=True) option4 = DictOption(item=StringOption()) option5 = DictOption(item=IntOption()) self.assertEqual(option1, option1) self.assertNotEqual(option1, option2) self.assertNotEqual(option1, option3) self.assertEqual(option1, option4) self.assertNotEqual(option1, option5) def test_to_string_when_json(self): option = DictOption() result = option.to_string({'foo': '1'}) self.assertEqual(result, '{"foo": "1"}') def test_to_string_when_no_json(self): option = DictOption(parse_json=False) result = option.to_string({'foo': '1'}) self.assertEqual(result, text_type({'foo': '1'})) class TestListOfDictOption(unittest.TestCase): def test_parse_lines_of_dict(self): """Test ListOption parse a list of dicts.""" class MySchema(Schema): foo = ListOption(item=DictOption( spec={ 'bar': StringOption(), 'baz': IntOption(), 'bla': BoolOption(), })) config = BytesIO(b"""[__main__] foo = mylist0 mylist1 [mylist0] bar=baz baz=42 bla=Yes [mylist1] bar=zort baz=123 bla=0 """) expected_values = { '__main__': {'foo': [{'bar': 'baz', 'baz': 42, 'bla': True}, {'bar': 'zort', 'baz': 123, 'bla': False}, ]}} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEqual(parser.values(), expected_values) class TestDictWithDicts(unittest.TestCase): """Test DictOption parse dict items.""" def test_parse_dict_with_dicts(self): innerspec = {'bar': StringOption(), 'baz': IntOption(), 'bla': BoolOption(), } spec = {'name': StringOption(), 'size': IntOption(), 'options': DictOption(spec=innerspec)} class MySchema(Schema): foo = DictOption(spec=spec) config = BytesIO(b"""[__main__] foo = outerdict [outerdict] options = innerdict [innerdict] bar = something baz = 42 """) expected_values = { '__main__': {'foo': {'name': '', 'size': 0, 'options': {'bar': 'something', 'baz': 42, 'bla': False}}}} schema = MySchema() parser = SchemaConfigParser(schema) parser.readfp(config) self.assertEqual(parser.values(), expected_values) class TestListOfTuples(unittest.TestCase): def setUp(self): class MySchema(Schema): foo = ListOption(item=TupleOption(length=3)) schema = MySchema() self.parser = SchemaConfigParser(schema) def test_parse_list_of_tuples(self): config = BytesIO(b'[__main__]\nfoo = a, b, c\n d, e, f') expected_values = { '__main__': {'foo': [('a', 'b', 'c'), ('d', 'e', 'f')]}} self.parser.readfp(config) self.assertEqual(self.parser.values(), expected_values) def test_parse_wrong_tuple_size(self): config = BytesIO(b'[__main__]\nfoo = a, b, c\n d, e') self.parser.readfp(config) self.assertRaises(ValueError, self.parser.values) def test_parse_empty_tuple(self): config = BytesIO(b'[__main__]\nfoo=()') expected_values = {'__main__': {'foo': [()]}} self.parser.readfp(config) self.assertEqual(self.parser.values(), expected_values) class TestSection(unittest.TestCase): cls = Section def test_default_name(self): """Test Section default name.""" section = self.cls() self.assertEqual(section.name, '') def test_custom_name(self): """Test Section custom name.""" section = self.cls(name='foo') self.assertEqual(section.name, 'foo') def test_equality(self): """Test Section equality.""" section1 = self.cls() section2 = self.cls() section3 = self.cls(name='foo') section4 = self.cls() section4.foo = IntOption() self.assertEqual(section1, section1) self.assertEqual(section1, section2) self.assertNotEqual(section1, section3) self.assertNotEqual(section1, section4) def test_repr(self): """Test Section repr.""" section1 = self.cls() section2 = self.cls(name='foo') self.assertEqual(repr(section1), '<{0}>'.format(self.cls.__name__)) self.assertEqual(repr(section2), '<{0} foo>'.format(self.cls.__name__)) def test_has_option(self): """Test Section has_option method.""" section = self.cls() section.foo = IntOption() section.bar = 4 self.assertEqual(section.has_option('foo'), True) self.assertEqual(section.has_option('bar'), False) def test_option(self): """Test Section option method.""" section = self.cls() section.foo = IntOption() self.assertEqual(section.option('foo'), section.foo) self.assertRaises(NoOptionError, section.option, 'bar') def test_options(self): """Test Section options method.""" section = self.cls() section.foo = IntOption() section.bar = 4 self.assertEqual(section.options(), [section.foo]) class MultiSchemaTestCase(unittest.TestCase): def test_merge_schemas_no_conflicts(self): class SchemaA(Schema): foo = IntOption() class SchemaB(Schema): bar = BoolOption() schema = merge(SchemaA, SchemaB)() self.assertEqual(set(s.name for s in schema.sections()), set(['__main__'])) self.assertEqual(set(o.name for o in schema.options('__main__')), set(['foo', 'bar'])) self.assertEqual(schema.foo, SchemaA().foo) self.assertEqual(schema.bar, SchemaB().bar) def test_merge_schemas_same_section(self): class SchemaA(Schema): class foo(Section): bar = IntOption() class SchemaB(Schema): class foo(Section): baz = BoolOption() schema = merge(SchemaA, SchemaB)() self.assertEqual(set(s.name for s in schema.sections()), set(['foo'])) self.assertEqual(set(o.name for o in schema.options('foo')), set(['bar', 'baz'])) self.assertEqual(schema.foo.bar, SchemaA().foo.bar) self.assertEqual(schema.foo.baz, SchemaB().foo.baz) def test_merge_schemas_duplicate(self): class SchemaA(Schema): foo = IntOption() class SchemaB(Schema): foo = IntOption() schema = merge(SchemaA, SchemaB)() self.assertEqual(set(s.name for s in schema.sections()), set(['__main__'])) self.assertEqual(set(o.name for o in schema.options('__main__')), set(['foo'])) self.assertEqual(schema.foo, SchemaA().foo) self.assertEqual(schema.foo, SchemaB().foo) def test_merge_schemas_conflicts(self): class SchemaA(Schema): foo = IntOption() class SchemaB(Schema): foo = BoolOption() try: merge(SchemaA, SchemaB) self.fail('SchemaValidationError not raised.') except SchemaValidationError as e: self.assertEqual(text_type(e), "Conflicting option '__main__.foo' while merging schemas.") configglue-1.1.2/configglue/tests/test_contrib_schema.py0000644000175000017500000000245212167556127024621 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from unittest import TestCase from configglue.schema import ListOption, StringOption from configglue.contrib.schema import DjangoOpenIdAuthSchema class DjangoOpenIdAuthSchemaTestCase(TestCase): def test_openid_launchpad_teams_required_option(self): schema = DjangoOpenIdAuthSchema() option = schema.openid.openid_launchpad_teams_required self.assertTrue(isinstance(option, ListOption)) self.assertTrue(isinstance(option.item, StringOption)) def test_openid_email_whitelist_regexp_list_option(self): schema = DjangoOpenIdAuthSchema() option = schema.openid.openid_email_whitelist_regexp_list self.assertTrue(isinstance(option, ListOption)) self.assertTrue(isinstance(option.item, StringOption)) configglue-1.1.2/configglue/app/0000755000175000017500000000000012167605161017634 5ustar ricardoricardo00000000000000configglue-1.1.2/configglue/app/plugin.py0000644000175000017500000000236012167555734021517 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### from configglue.schema import Schema __all__ = [ 'Plugin', 'PluginManager', ] class Plugin(object): schema = Schema enabled = False class PluginManager(object): def __init__(self): self.available = self.load() @property def enabled(self): return [cls for cls in self.available if cls.enabled] def enable(self, plugin): plugin.enabled = True def disable(self, plugin): plugin.enabled = False @property def schemas(self): return [cls.schema for cls in self.enabled] def load(self): return [] def register(self, plugin): if plugin not in self.available: self.available.append(plugin) configglue-1.1.2/configglue/app/__init__.py0000644000175000017500000000115712167555732021761 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### # import into local namespace from .base import * from .plugin import * configglue-1.1.2/configglue/app/base.py0000644000175000017500000000507412167555730021134 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### import os.path import sys from optparse import OptionParser from xdg.BaseDirectory import load_config_paths from configglue.glue import configglue from configglue.schema import ( Schema, merge, ) from .plugin import PluginManager __all__ = [ 'App', ] class Config(object): def __init__(self, app): schemas = [app.schema] + app.plugins.schemas self.schema = merge(*schemas) # initialize config config_files = self.get_config_files(app) self.glue = configglue(self.schema, config_files, op=app.parser) def get_config_files(self, app): config_files = [] for path in reversed(list(load_config_paths(app.name))): self._add_config_file(config_files, path, app.name) for plugin in app.plugins.enabled: self._add_config_file(config_files, path, plugin.__name__) self._add_config_file(config_files, '.', 'local') return config_files def _add_config_file(self, config_files, path, name): filename = os.path.join(path, "{0}.cfg".format(name.lower())) if os.path.exists(filename): config_files.append(filename) class App(object): schema = Schema plugin_manager = PluginManager def __init__(self, schema=None, plugin_manager=None, name=None, parser=None): # initialize app name if name is None: name = os.path.splitext(os.path.basename(sys.argv[0]))[0] self.name = name # setup plugins if plugin_manager is None: self.plugins = self.plugin_manager() else: self.plugins = plugin_manager # setup schema if schema is not None: self.schema = schema # setup option parser if parser is None: parser = OptionParser() parser.add_option('--validate', dest='validate', default=False, action='store_true', help="validate configuration") self.parser = parser # setup config self.config = Config(self) configglue-1.1.2/configglue/_compat.py0000644000175000017500000001161712167601026021052 0ustar ricardoricardo00000000000000# -*- coding: utf-8 -*- ############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # (C) Python Software Foundation (“PSF”) # # Released under the PSF License Agreement (see the file LICENSE.PSF) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### import re import sys PY2 = sys.version_info[0] == 2 if PY2: import __builtin__ as builtins import ConfigParser as configparser from ConfigParser import ( DEFAULTSECT, InterpolationDepthError, InterpolationMissingOptionError, InterpolationSyntaxError, NoOptionError, NoSectionError, RawConfigParser, ) class BasicInterpolation(object): """Interpolation as implemented in the classic ConfigParser. The option values can contain format strings which refer to other values in the same section, or values in the special default section. For example: something: %(dir)s/whatever would resolve the "%(dir)s" to the value of dir. All reference expansions are done late, on demand. If a user needs to use a bare % in a configuration file, she can escape it by writing %%. Other % usage is considered a user error and raises `InterpolationSyntaxError'.""" _KEYCRE = re.compile(r"%\(([^)]+)\)s") def before_get(self, parser, section, option, value, defaults): L = [] self._interpolate_some(parser, option, L, value, section, defaults, 1) return ''.join(L) def before_set(self, parser, section, option, value): tmp_value = value.replace('%%', '') # escaped percent signs tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax if '%' in tmp_value: raise ValueError("invalid interpolation syntax in %r at " "position %d" % (value, tmp_value.find('%'))) return value def _interpolate_some(self, parser, option, accum, rest, section, map, depth): if depth > configparser.MAX_INTERPOLATION_DEPTH: raise configparser.InterpolationDepthError(option, section, rest) while rest: p = rest.find("%") if p < 0: accum.append(rest) return if p > 0: accum.append(rest[:p]) rest = rest[p:] # p is no longer used c = rest[1:2] if c == "%": accum.append("%") rest = rest[2:] elif c == "(": m = self._KEYCRE.match(rest) if m is None: raise configparser.InterpolationSyntaxError(option, section, "bad interpolation variable reference %r" % rest) var = parser.optionxform(m.group(1)) rest = rest[m.end():] try: v = map[var] except KeyError: raise configparser.InterpolationMissingOptionError( option, section, rest, var) if "%" in v: self._interpolate_some(parser, option, accum, v, section, map, depth + 1) else: accum.append(v) else: raise configparser.InterpolationSyntaxError( option, section, "'%%' must be followed by '%%' or '(', " "found: %r" % (rest,)) class BaseConfigParser(configparser.SafeConfigParser): def __init__(self, *args, **kwargs): configparser.SafeConfigParser.__init__(self, *args, **kwargs) self._interpolation = BasicInterpolation() text_type = unicode string_types = (str, unicode) iteritems = lambda d: d.iteritems() else: import builtins import configparser from configparser import ( DEFAULTSECT, InterpolationDepthError, InterpolationMissingOptionError, InterpolationSyntaxError, NoOptionError, NoSectionError, RawConfigParser, ) BaseConfigParser = configparser.SafeConfigParser text_type = str string_types = (str,) iteritems = lambda d: iter(d.items()) configglue-1.1.2/configglue/parser.py0000644000175000017500000005645212167561172020741 0ustar ricardoricardo00000000000000############################################################################### # # configglue -- glue for your apps' configuration # # A library for simple, DRY configuration of applications # # (C) 2009--2013 by Canonical Ltd. # by John R. Lenton # and Ricardo Kirkner # # Released under the BSD License (see the file LICENSE) # # For bug reports, support, and new releases: http://launchpad.net/configglue # ############################################################################### import codecs import collections import copy import logging import os import re from functools import reduce from ._compat import BaseConfigParser, text_type, string_types from ._compat import ( DEFAULTSECT, InterpolationMissingOptionError, NoOptionError, NoSectionError, ) __all__ = [ 'SchemaValidationError', 'SchemaConfigParser', ] CONFIG_FILE_ENCODING = 'utf-8' class NullHandler(logging.Handler): def emit(self, record): pass logger = logging.getLogger(__name__) logger.addHandler(NullHandler()) class SchemaValidationError(Exception): """Exception class raised for any schema validation error.""" class SchemaConfigParser(BaseConfigParser, object): """A ConfigParser that validates against a Schema The way to use this class is: config = SchemaConfigParser(MySchema()) config.read('mysystemconfig.cfg', 'mylocalconfig.cfg', ...) config.parse_all() ... profit! """ def __init__(self, schema): super(SchemaConfigParser, self).__init__() # validate schema if not schema.is_valid(): # TODO: add error details raise SchemaValidationError() self.schema = schema self._location = {} self.extra_sections = set() self._basedir = '' self._dirty = collections.defaultdict( lambda: collections.defaultdict(dict)) def is_valid(self, report=False): """Return if the state of the parser is valid. This is useful to detect errors in configuration files, like type errors or missing required options. """ valid = True errors = [] try: # validate structure config_sections = set(self.sections()) schema_sections = set(s.name for s in self.schema.sections()) skip_sections = self.extra_sections magic_sections = set(['__main__', '__noschema__']) # test1: no undefined implicit sections unmatched_sections = (skip_sections - config_sections) if unmatched_sections: error_msg = "Undefined sections in configuration: %s" error_value = ', '.join(unmatched_sections) errors.append(error_msg % error_value) valid = False # remove sections to skip from config sections config_sections.difference_update(skip_sections) # test2: no extra sections that are not implicit sections unmatched_sections = ( config_sections - magic_sections - schema_sections) if unmatched_sections: error_msg = "Sections in configuration are missing from schema: %s" error_value = ', '.join(unmatched_sections) errors.append(error_msg % error_value) valid = False for name in config_sections.union(schema_sections): if name not in skip_sections: if not self.schema.has_section(name): # this should have been reported before # so skip bogus section continue section = self.schema.section(name) try: parsed_options = set(self.options(name)) except NoSectionError: parsed_options = set([]) schema_options = set(section.options()) fatal_options = set(opt.name for opt in schema_options if opt.fatal) # all fatal options are included fatal_included = parsed_options.issuperset(fatal_options) if not fatal_included: error_msg = ("Configuration missing required options" " for section '%s': %s") error_value = ', '.join(list(fatal_options - parsed_options)) errors.append(error_msg % (name, error_value)) valid &= fatal_included # remaining parsed options are valid schema options other_options = parsed_options - fatal_options schema_opt_names = set(opt.name for opt in schema_options) # add the default section special includes option if name == '__main__': schema_opt_names.add('includes') schema_options = other_options.issubset(schema_opt_names) if not schema_options: error_msg = ("Configuration includes invalid options" " for section '%s': %s") error_value = ', '.join(list(other_options - schema_opt_names)) errors.append(error_msg % (name, error_value)) valid &= schema_options # structure validates, validate content self.parse_all() except Exception as e: errors.append(text_type(e)) valid = False if report: return valid, errors else: return valid def items(self, section, raw=False, vars=None): """Return the list of all options in a section. The returned value is a list of tuples of (name, value) for each option in the section. All % interpolations are expanded in the return values, based on the defaults passed into the constructor, unless the optional argument `raw' is true. Additional substitutions may be provided using the `vars' argument, which must be a dictionary whose contents overrides any pre-existing defaults. The section __main__ is special as it's implicitly defined and used for options not explicitly included in any section. """ d = self._defaults.copy() try: d.update(self._sections[section]) except KeyError: if section != DEFAULTSECT: raise NoSectionError(section) # Update with the entry specific variables if vars: for key, value in vars.items(): d[self.optionxform(key)] = value options = d.keys() if "__name__" in options: options.remove("__name__") if raw: return [(option, d[option]) for option in options] else: items = [] for option in options: try: value = self._interpolate(section, option, d[option], d) except InterpolationMissingOptionError as e: # interpolation failed, because key was not found in # section. try other sections before bailing out value = self._interpolate_value(section, option) if value is None: # this should be a string, so None indicates an error raise e items.append((option, value)) return items def values(self, section=None, parse=True): """Returns multiple values in a dict. This method can return the value of multiple options in a single call, unlike get() that returns a single option's value. If section=None, return all options from all sections. If section is specified, return all options from that section only. Section is to be specified *by name*, not by passing in real Section objects. """ values = collections.defaultdict(dict) if section is None: sections = self.schema.sections() else: sections = [self.schema.section(section)] for sect in sections: for opt in sect.options(): values[sect.name][opt.name] = self.get( sect.name, opt.name, parse=parse) if section is not None: return values[section] else: return values def read(self, filenames, already_read=None): """Like ConfigParser.read, but consider files we've already read.""" if already_read is None: already_read = set() if isinstance(filenames, string_types): filenames = [filenames] read_ok = [] for filename in filenames: path = os.path.join(self._basedir, filename) if path in already_read: continue try: fp = codecs.open(path, 'r', encoding=CONFIG_FILE_ENCODING) except IOError: logger.warn( 'File {0} could not be read. Skipping.'.format(path)) continue self._read(fp, path, already_read=already_read) fp.close() read_ok.append(path) self._last_location = filename return read_ok def readfp(self, fp, filename=None): """Like ConfigParser.readfp, but consider the encoding.""" # wrap the StringIO so it can read encoded text decoded_fp = codecs.getreader(CONFIG_FILE_ENCODING)(fp) self._read(decoded_fp, filename) def _read(self, fp, fpname, already_read=None): # read file content self._update(fp, fpname) if already_read is None: already_read = set() already_read.add(fpname) if self.has_option('__main__', 'includes'): old_basedir, self._basedir = self._basedir, os.path.dirname( fpname) includes = self.get('__main__', 'includes') filenames = [text_type.strip(x) for x in includes] self.read(filenames, already_read=already_read) self._basedir = old_basedir if filenames: # re-read the file to override included options with # local values fp.seek(0) self._update(fp, fpname) def _update(self, fp, fpname): # remember current values old_sections = copy.deepcopy(self._sections) # read in new file super(SchemaConfigParser, self)._read(fp, fpname) # update location of changed values self._update_location(old_sections, fpname) def _update_location(self, old_sections, filename): # keep list of valid options to include locations for option_names = [x.name for x in self.schema.options()] # new values sections = self._sections # update locations for section, options in sections.items(): old_section = old_sections.get(section) if old_section is not None: # update options in existing section for option, value in options.items(): valid_option = option in option_names option_changed = (option not in old_section or value != old_section[option]) if valid_option and option_changed: self._location[option] = filename else: # complete section is new for option, value in options.items(): valid_option = option in option_names if valid_option: self._location[option] = filename def parse(self, section, option, value): """Parse the value of an option. This method raises NoSectionError if an invalid section name is passed in. This method raises ValueError if the value is not parseable. """ if section == '__main__': option_obj = getattr(self.schema, option, None) else: section_obj = getattr(self.schema, section, None) if section_obj is not None: option_obj = getattr(section_obj, option, None) else: raise NoSectionError(section) if option_obj is not None: kwargs = {} if option_obj.require_parser: kwargs = {'parser': self} try: value = option_obj.parse(value, **kwargs) except ValueError as e: raise ValueError("Invalid value '%s' for %s '%s' in" " section '%s'. Original exception was: %s" % (value, option_obj.__class__.__name__, option, section, e)) return value def parse_all(self): """Go through all sections and options attempting to parse each one. If any options are omitted from the config file, provide the default value from the schema. In the case of an NoSectionError or NoOptionError, raise it if the option has *fatal* set to *True*. """ for section in self.schema.sections(): for option in section.options(): try: self.get(section.name, option.name, raw=option.raw) except (NoSectionError, NoOptionError): if option.fatal: raise def locate(self, option=None): """Return the location (file) where the option was last defined.""" return self._location.get(option) def _extract_interpolation_keys(self, item): if isinstance(item, (list, tuple)): keys = [self._extract_interpolation_keys(x) for x in item] keys = reduce(set.union, keys, set()) else: keys = set(self._interpolation._KEYCRE.findall(item)) # remove invalid key if '' in keys: keys.remove('') return keys def _get_interpolation_keys(self, section, option): rawval = super(SchemaConfigParser, self).get(section, option, raw=True) try: opt = self.schema.section(section).option(option) value = opt.parse(rawval, raw=True) except: value = rawval keys = self._extract_interpolation_keys(value) return rawval, keys def _interpolate(self, *args, **kwargs): """Helper method for transition to configparser.""" return self._interpolation.before_get(self, *args, **kwargs) def _interpolate_value(self, section, option): rawval, keys = self._get_interpolation_keys(section, option) if not keys: # interpolation keys are not valid return values = {} # iterate over the other sections for key in keys: # we want the unparsed value try: value = self.get(section, key, parse=False) except (NoSectionError, NoOptionError): # value of key not found in config, so try in special # sections for section in ('__main__', '__noschema__'): try: value = super(SchemaConfigParser, self).get(section, key) break except: continue else: return values[key] = value # replace holders with values result = rawval % values assert isinstance(result, string_types) return result def interpolate_environment(self, rawval, raw=False): """Interpolate environment variables""" if raw: return rawval # this allows both nested and mutliple environment variable # interpolation in a single value pattern = re.sub(r'\${([A-Z_]+)}', r'%(\1)s', rawval) pattern = re.sub(r'\$([A-Z_]+)', r'%(\1)s', pattern) # counter to protect against infinite loops num_interpolations = 0 # save simple result in case we need it simple_pattern = pattern # handle complex case of env vars with defaults env = os.environ.copy() env_re = re.compile(r'\${(?P[A-Z_]+):-(?P.*?)}') match = env_re.search(pattern) while match and num_interpolations < 50: groups = match.groupdict() name = groups['name'] pattern = pattern.replace(match.group(), '%%(%s)s' % name) if name not in env and groups['default'] is not None: # interpolate defaults as well to allow ${FOO:-$BAR} env[name] = groups['default'] % os.environ num_interpolations += 1 match = env_re.search(pattern) if num_interpolations >= 50: # blown loop, restore earlier simple interpolation pattern = simple_pattern keys = self._extract_interpolation_keys(pattern) if not keys: # interpolation keys are not valid return rawval return pattern % env def _get_default(self, section, option): # cater for 'special' sections if section == '__noschema__': value = super(SchemaConfigParser, self).get(section, option) return value # any other section opt = self.schema.section(section).option(option) if not opt.fatal: value = opt.default return value # no default value found, raise an error raise NoOptionError(option, section) def get(self, section, option, raw=False, vars=None, parse=True): """Return the parsed value of an option. If *raw* is True, return the value as it's stored in the parser, without parsing it. Extra *vars* can be passed in to be evaluated during parsing. If *parse* is False, return the string representation of the value. """ try: # get option's raw mode setting try: option_obj = self._get_option(section, option) raw = option_obj.raw or raw except: pass # value is defined entirely in current section value = super(SchemaConfigParser, self).get(section, option, raw=raw, vars=vars) except InterpolationMissingOptionError as e: # interpolation key not in same section value = self._interpolate_value(section, option) if value is None: # this should be a string, so None indicates an error raise e except (NoSectionError, NoOptionError) as e: # option not found in config, try to get its default value from # schema value = self._get_default(section, option) # interpolate environment variables if isinstance(value, string_types): try: value = self.interpolate_environment(value, raw=raw) if parse: value = self.parse(section, option, value) except KeyError: # interpolation failed, fallback to default value value = self._get_default(section, option) return value def _get_option(self, section, option): section_obj = self.schema.section(section) option_obj = section_obj.option(option) return option_obj def set(self, section, option, value): """Set an option's raw value.""" option_obj = self._get_option(section, option) # make sure the value is of the right type for the option if not option_obj.validate(value): raise TypeError("{0} is not a valid {1} value.".format( value, type(option_obj).__name__)) # cast value to a string because SafeConfigParser only allows # strings to be set str_value = option_obj.to_string(value) if not self.has_section(section): # Don't call .add_section here because 2.6 complains # about sections called '__main__' self._sections[section] = {} super(SchemaConfigParser, self).set(section, option, str_value) filename = self.locate(option) self._dirty[filename][section][option] = str_value def write(self, fp): """Write an .ini-format representation of the configuration state.""" # make sure the parser is populated self._fill_parser() if self._defaults: fp.write("[%s]\n" % DEFAULTSECT) for (key, value) in self._defaults.items(): fp.write("%s = %s\n" % (key, value.replace('\n', '\n\t'))) fp.write("\n") for section in self._sections: fp.write("[%s]\n" % section) for (key, value) in self._sections[section].items(): if key == "__name__": continue if (value is not None) or (self._optcre == self.OPTCRE): key = " = ".join((key, value.replace('\n', '\n\t'))) fp.write("%s\n" % (key)) fp.write("\n") def save(self, fp=None): """Save the parser contents to a file. The data will be saved as a ini file. """ if fp is not None: if isinstance(fp, string_types): fp = codecs.open(fp, 'w', encoding=CONFIG_FILE_ENCODING) self.write(fp) else: # write to the original files for filename, sections in self._dirty.items(): if filename is None: # default option was overridden. figure out where to # save it if not getattr(self, '_last_location', None): # no files where read, there is no possible # location for the customized setting. raise ValueError("No config files where read and no " "location was specified for writing the " "configuration.") else: filename = self._last_location parser = BaseConfigParser() parser.read(filename) for section, options in sections.items(): for option, value in options.items(): parser.set(section, option, value) # write to new file parser.write(codecs.open("%s.new" % filename, 'w', encoding=CONFIG_FILE_ENCODING)) # rename old file if os.path.exists(filename): os.rename(filename, "%s.old" % filename) # rename new file os.rename("%s.new" % filename, filename) def _fill_parser(self): """Populate parser with current values for each option.""" values = self.values() for section, options in values.items(): for option, value in options.items(): self.set(section, option, value) # make sure having set the options didn't change anything assert values == self.values() configglue-1.1.2/MANIFEST.in0000644000175000017500000000002012147251621016454 0ustar ricardoricardo00000000000000include LICENSE configglue-1.1.2/README0000644000175000017500000000213212147251621015604 0ustar ricardoricardo00000000000000configglue -- glue for your apps' configuration Three things: AttributedConfigParser: a ConfigParser that gives you attributed options. So a config file with [foo] bar = Hello World bar.level = newbie will have one option under the 'foo' section, and that option will have a value ('Hello World') and an attribute 'level', with value 'newbie'. TypedConfigParser: an AttributedConfigParser that uses the 'parser' attribtue to parse the value. So [foo] bar = 7 bar.parser = int will have a 'foo' section with a 'bar' option which value is int('7'). configglue: A function that creates an TypedConfigParser and uses it to build an optparse.OptionParser instance. So you can have a config file with [foo] bar.default = 7 bar.help = The bar number [%(default)s] bar.metavar = BAR bar.parser = int and if you load it with configglue, you get something like $ python sample.py --help Usage: sample.py [options] Options: -h, --help show this help message and exit blah: --foo-bar=BAR The bar number [7] configglue-1.1.2/configglue.egg-info/0000755000175000017500000000000012167605161020546 5ustar ricardoricardo00000000000000configglue-1.1.2/configglue.egg-info/PKG-INFO0000644000175000017500000000205612167605160021645 0ustar ricardoricardo00000000000000Metadata-Version: 1.1 Name: configglue Version: 1.1.2 Summary: Glue to stick OptionParser and ConfigParser together Home-page: https://launchpad.net/configglue Author: John R. Lenton, Ricardo Kirkner Author-email: john.lenton@canonical.com, ricardo.kirkner@canonical.com License: BSD License Description: configglue is a library that glues together python's optparse.OptionParser and ConfigParser.ConfigParser, so that you don't have to repeat yourself when you want to export the same options to a configuration file and a commandline interface. Platform: UNKNOWN Classifier: License :: OSI Approved :: BSD License Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 configglue-1.1.2/configglue.egg-info/top_level.txt0000644000175000017500000000001312167605160023271 0ustar ricardoricardo00000000000000configglue configglue-1.1.2/configglue.egg-info/SOURCES.txt0000644000175000017500000000273012167605161022434 0ustar ricardoricardo00000000000000LICENSE MANIFEST.in README setup.cfg setup.py configglue/__init__.py configglue/_compat.py configglue/glue.py configglue/parser.py configglue/schema.py configglue.egg-info/PKG-INFO configglue.egg-info/SOURCES.txt configglue.egg-info/dependency_links.txt configglue.egg-info/requires.txt configglue.egg-info/top_level.txt configglue.egg-info/zip-safe configglue/app/__init__.py configglue/app/base.py configglue/app/plugin.py configglue/contrib/__init__.py configglue/contrib/schema/__init__.py configglue/contrib/schema/devserver.py configglue/contrib/schema/django_jenkins.py configglue/contrib/schema/django_openid_auth.py configglue/contrib/schema/nexus.py configglue/contrib/schema/preflight.py configglue/contrib/schema/pystatsd.py configglue/contrib/schema/raven.py configglue/contrib/schema/saml2idp.py configglue/inischema/__init__.py configglue/inischema/attributed.py configglue/inischema/glue.py configglue/inischema/parsers.py configglue/inischema/typed.py configglue/tests/__init__.py configglue/tests/test_contrib_schema.py configglue/tests/test_parser.py configglue/tests/test_schema.py configglue/tests/test_schemaconfig.py configglue/tests/app/__init__.py configglue/tests/app/test_base.py configglue/tests/app/test_plugin.py configglue/tests/inischema/__init__.py configglue/tests/inischema/test_attributed.py configglue/tests/inischema/test_glue.py configglue/tests/inischema/test_glue2glue.py configglue/tests/inischema/test_parsers.py configglue/tests/inischema/test_typed.pyconfigglue-1.1.2/configglue.egg-info/dependency_links.txt0000644000175000017500000000005712167605160024626 0ustar ricardoricardo00000000000000http://www.freedesktop.org/wiki/Software/pyxdg configglue-1.1.2/configglue.egg-info/zip-safe0000644000175000017500000000000112147251675022203 0ustar ricardoricardo00000000000000 configglue-1.1.2/configglue.egg-info/requires.txt0000644000175000017500000000000512167605160023140 0ustar ricardoricardo00000000000000pyxdg