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