├── .editorconfig ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── BADGES.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── appveyor.yml ├── docs ├── Makefile ├── RELEASE.rst ├── api.rst ├── argparse.rst ├── conf.py ├── configsource.rst ├── examples │ ├── argparse-example.py │ ├── conf.py │ ├── defaults.py │ ├── firststep.py │ ├── myapp.ini │ ├── pyfile-example.py │ ├── pyfile-example2.py │ └── usage.py ├── index.rst ├── layeredconfig.rst ├── make.bat ├── pyfile.rst ├── readme.rst ├── sources.rst └── usage.rst ├── layeredconfig ├── __init__.py ├── commandline.py ├── configsource.py ├── defaults.py ├── dictsource.py ├── environment.py ├── etcdstore.py ├── inifile.py ├── jsonfile.py ├── layeredconfig.py ├── plistfile.py ├── pyfile.py └── yamlfile.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_examples.py ├── test_layeredconfig.py └── test_withFuture.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | htmlcov 29 | .cache 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Complexity 40 | output/*.html 41 | output/*/index.html 42 | 43 | # Sphinx 44 | docs/_build 45 | 46 | # Pycharm 47 | .idea/ 48 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | language: python 4 | 5 | python: 6 | - "3.7" 7 | - "3.6" 8 | - "3.5" 9 | - "3.4" 10 | - "2.7" 11 | 12 | before_install: 13 | - curl -L https://github.com/coreos/etcd/releases/download/v3.0.4/etcd-v3.0.4-linux-amd64.tar.gz -o etcd-v3.0.4-linux-amd64.tar.gz 14 | - tar xzvf etcd-v3.0.4-linux-amd64.tar.gz 15 | - cd etcd-v3.0.4-linux-amd64 16 | - ./etcd > /dev/null & 17 | - cd .. 18 | 19 | install: 20 | - pip install -r requirements.txt 21 | - pip install coverage==3.7.1 coveralls 22 | 23 | before_script: 24 | - sleep 10 25 | 26 | # command to run tests, e.g. python setup.py test 27 | script: 28 | - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then PYTHONWARNINGS=i coverage run --include "layeredconfig/*py" -m unittest2 discover tests; fi 29 | - if [[ $TRAVIS_PYTHON_VERSION != '2.6' ]]; then PYTHONWARNINGS=i coverage run --include "layeredconfig/*py" -m unittest discover tests; fi 30 | after_success: 31 | - coveralls 32 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Staffan Malmgren 9 | 10 | Contributors 11 | ------------ 12 | 13 | * https://github.com/badkapitan 14 | -------------------------------------------------------------------------------- /BADGES.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://badge.fury.io/py/layeredconfig.png 2 | :target: http://badge.fury.io/py/layeredconfig 3 | 4 | .. image:: https://travis-ci.org/staffanm/layeredconfig.png?branch=master 5 | :target: https://travis-ci.org/staffanm/layeredconfig 6 | 7 | .. image:: https://ci.appveyor.com/api/projects/status/nnfqv9jhxh3afgn0/branch/master 8 | :target: https://ci.appveyor.com/project/staffanm/layeredconfig/branch/master 9 | 10 | .. image:: https://coveralls.io/repos/staffanm/layeredconfig/badge.png?branch=master 11 | :target: https://coveralls.io/r/staffanm/layeredconfig 12 | 13 | .. image:: https://landscape.io/github/staffanm/layeredconfig/master/landscape.png 14 | :target: https://landscape.io/github/staffanm/layeredconfig/master 15 | :alt: Code Health 16 | 17 | .. image:: https://pypip.in/d/layeredconfig/badge.png 18 | :target: https://pypi.python.org/pypi/layeredconfig 19 | 20 | Full documentation: https://layeredconfig.readthedocs.org/ 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | You can contribute in many ways: 9 | 10 | Types of Contributions 11 | ---------------------- 12 | 13 | Report Bugs 14 | ~~~~~~~~~~~ 15 | 16 | Report bugs at https://github.com/staffanm/layeredconfig/issues. 17 | 18 | If you are reporting a bug, please include: 19 | 20 | * Your operating system name and version. 21 | * Any details about your local setup that might be helpful in troubleshooting. 22 | * Detailed steps to reproduce the bug. 23 | 24 | Fix Bugs 25 | ~~~~~~~~ 26 | 27 | Look through the GitHub issues for bugs. Anything tagged with "bug" 28 | is open to whoever wants to implement it. 29 | 30 | Implement Features 31 | ~~~~~~~~~~~~~~~~~~ 32 | 33 | Look through the GitHub issues for features. Anything tagged with "feature" 34 | is open to whoever wants to implement it. 35 | 36 | Write Documentation 37 | ~~~~~~~~~~~~~~~~~~~ 38 | 39 | LayeredConfig could always use more documentation, whether as part of the 40 | official LayeredConfig docs, in docstrings, or even on the web in blog posts, 41 | articles, and such. 42 | 43 | Submit Feedback 44 | ~~~~~~~~~~~~~~~ 45 | 46 | The best way to send feedback is to file an issue at https://github.com/staffanm/layeredconfig/issues. 47 | 48 | If you are proposing a feature: 49 | 50 | * Explain in detail how it would work. 51 | * Keep the scope as narrow as possible, to make it easier to implement. 52 | * Remember that this is a volunteer-driven project, and that contributions 53 | are welcome :) 54 | 55 | Get Started! 56 | ------------ 57 | 58 | Ready to contribute? Here's how to set up `layeredconfig` for local development. 59 | 60 | 1. Fork the `layeredconfig` repo on GitHub. 61 | 2. Clone your fork locally:: 62 | 63 | $ git clone git@github.com:your_name_here/layeredconfig.git 64 | 65 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 66 | 67 | $ mkvirtualenv layeredconfig 68 | $ cd layeredconfig/ 69 | $ python setup.py develop 70 | 71 | 4. Create a branch for local development:: 72 | 73 | $ git checkout -b name-of-your-bugfix-or-feature 74 | 75 | Now you can make your changes locally. 76 | 77 | 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: 78 | 79 | $ flake8 layeredconfig tests 80 | $ python setup.py test 81 | $ tox 82 | 83 | To get flake8 and tox, just pip install them into your virtualenv. 84 | 85 | 6. Commit your changes and push your branch to GitHub:: 86 | 87 | $ git add . 88 | $ git commit -m "Your detailed description of your changes." 89 | $ git push origin name-of-your-bugfix-or-feature 90 | 91 | 7. Submit a pull request through the GitHub website. 92 | 93 | Pull Request Guidelines 94 | ----------------------- 95 | 96 | Before you submit a pull request, check that it meets these guidelines: 97 | 98 | 1. The pull request should include tests. 99 | 2. If the pull request adds functionality, the docs should be updated. Put 100 | your new functionality into a function with a docstring, and add the 101 | feature to the list in README.rst. 102 | 3. The pull request should work for Python 2.6, 2.7, 3.2, 3.3, and 3.4, and for PyPy. Check 103 | https://travis-ci.org/staffanm/layeredconfig/pull_requests 104 | and make sure that the tests pass for all supported Python versions. 105 | 106 | Tips 107 | ---- 108 | 109 | To run a subset of tests:: 110 | 111 | $ python -m unittest tests.test_layeredconfig 112 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ======= 5 | 6 | 0.3.3 (2019-11-11) 7 | ------------------ 8 | 9 | * INIFile has been extended to support nested subsections. 10 | * Fixed a problem with DictSource (and by extension the Defaults 11 | source) where nested sections could be erroneously handled. 12 | * The sectionsep parameter to the Environment constructor has been 13 | documented. Thanks to @brentmclark for the patch! 14 | * Using "_" in the name of a configuration setting was not supported, 15 | regardless of what sectionsep had been set to. Thanks to @dginzbourg 16 | for reporting this! 17 | * Support for python 3.3 and 2.6 was dropped. 18 | 19 | 20 | 0.3.2 (2016-09-26) 21 | ------------------ 22 | 23 | * Fixed bug #9 (Custom section separators caused values to be 24 | retrieved as lists, not single values). Thanks to @numbnut for 25 | reporting this! 26 | 27 | 0.3.1 (2016-08-31) 28 | ------------------ 29 | 30 | * Fixed bug #8 (layering a Commandline source over a YAMLFile with 31 | defined subsection resulted in crash in initialization. Thanks to 32 | @AnsonT for reporting this! 33 | * The default URI used for EtcdStore was changed to reflect that port 34 | 2379 should be used instead of 4001 (which was the default for etcd 35 | 1.*). 36 | * Support for Python 3.2 was dropped. 37 | 38 | 0.3.0 (2016-08-06) 39 | ------------------ 40 | 41 | * New staticmethod ``dump``, which returns the content of the passed 42 | config object as a dict. This is also used as a printable 43 | representation of a config object (through ``__repr__``). 44 | * The intrinsic type of any typed setting may not be None any longer. 45 | * If you subclass LayeredConfig, any created subsection will be 46 | instances of your subclass, not the base LayeredConfig class 47 | * Layering multiple configuration files now works even when earlier 48 | files might lack subsections present in latter. 49 | 50 | All of the above was done by @jedipi. Many thanks! 51 | 52 | A number of unreported bugs, mostly concerning unicode handling and 53 | type conversion in various sources, was also fixed. 54 | 55 | 0.2.2 (2016-01-24) 56 | ------------------ 57 | 58 | * Fixed a bug when using a class in a Default configuration for 59 | automatic coercion, where the type of the class isn't type (as is 60 | the case with the "newint" and other classes from the future 61 | module). 62 | 63 | * Fixed a bug where loading configuration from multiple config files 64 | would crash if latter configs lacked subsections present in 65 | earlier. Thanks to @badkapitan! 66 | 67 | 0.2.1 (2014-11-26) 68 | ------------------ 69 | 70 | * Made the Commandline source interact better with "partially 71 | configured" ArgumentParser objects (parsers that has been configured 72 | with some, but not all, possible arguments). 73 | 74 | 0.2.0 (2014-11-22) 75 | ------------------ 76 | 77 | * Integration with argparse: The Commandline source now accepts an 78 | optional parse parameter, which should be a configured 79 | argparse.ArgumentParser object. Most features of argparse, such as 80 | specifying the type of arguments, and automatic help text 81 | * A new source, PyFile, for reading configuration from python source 82 | files. 83 | * Another new source, EtcdStore, for reading configuration from etcd 84 | stores. 85 | 86 | 0.1.0 (2014-11-03) 87 | ------------------ 88 | 89 | * First release on PyPI. 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Staffan Malmgren 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of LayeredConfig nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | include BADGES.rst 7 | 8 | recursive-include tests * 9 | recursive-exclude * __pycache__ 10 | recursive-exclude * *.py[co] 11 | 12 | recursive-include docs *.rst conf.py Makefile make.bat -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build docs clean 2 | 3 | help: 4 | @echo "clean - remove all build, test, coverage and Python artifacts" 5 | @echo "clean-build - remove build artifacts" 6 | @echo "clean-pyc - remove Python file artifacts" 7 | @echo "clean-test - remove test and coverage artifacts" 8 | @echo "lint - check style with flake8" 9 | @echo "test - run tests quickly with the default Python" 10 | @echo "test-all - run tests on every Python version with tox" 11 | @echo "coverage - check code coverage quickly with the default Python" 12 | @echo "docs - generate Sphinx HTML documentation, including API docs" 13 | @echo "release - package and upload a release" 14 | @echo "dist - package" 15 | 16 | clean: clean-build clean-pyc clean-test 17 | 18 | clean-build: 19 | rm -fr build/ 20 | rm -fr dist/ 21 | rm -fr *.egg-info 22 | 23 | clean-pyc: 24 | find . -name '*.pyc' -exec rm -f {} + 25 | find . -name '*.pyo' -exec rm -f {} + 26 | find . -name '*~' -exec rm -f {} + 27 | find . -name '__pycache__' -exec rm -fr {} + 28 | 29 | clean-test: 30 | rm -fr .tox/ 31 | rm -f .coverage 32 | rm -fr htmlcov/ 33 | 34 | lint: 35 | flake8 layeredconfig tests 36 | 37 | test: 38 | python setup.py test 39 | 40 | test-all: 41 | tox 42 | 43 | coverage: 44 | coverage run --source layeredconfig setup.py test 45 | coverage report -m 46 | coverage html 47 | open htmlcov/index.html 48 | 49 | docs: 50 | rm -f docs/layeredconfig.rst 51 | rm -f docs/modules.rst 52 | sphinx-apidoc -o docs/ layeredconfig 53 | $(MAKE) -C docs clean 54 | $(MAKE) -C docs html 55 | open docs/_build/html/index.html 56 | 57 | release: clean 58 | python setup.py sdist upload 59 | python setup.py bdist_wheel upload 60 | 61 | dist: clean 62 | python setup.py sdist 63 | python setup.py bdist_wheel 64 | ls -l dist -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | LayeredConfig compiles configuration from files, environment 2 | variables, command line arguments, hard-coded default values, or other 3 | backends, and makes it available to your code in a simple way:: 4 | 5 | from layeredconfig import (LayeredConfig, Defaults, INIFile, 6 | Environment, Commandline) 7 | 8 | # This represents four different way of specifying the value of the 9 | # configuration option "hello": 10 | 11 | # 1. hard-coded defaults 12 | defaults = {"hello": "is it me you're looking for?"} 13 | 14 | # 2. INI configuration file 15 | with open("myapp.ini", "w") as fp: 16 | fp.write(""" 17 | [__root__] 18 | hello = kitty 19 | """) 20 | 21 | # 3. enironment variables 22 | import os 23 | os.environ['MYAPP_HELLO'] = 'goodbye' 24 | 25 | # 4.command-line arguments 26 | import sys 27 | sys.argv = ['./myapp.py', '--hello=world'] 28 | 29 | # Create a config object that gets settings from these four 30 | # sources. 31 | config = LayeredConfig(Defaults(defaults), 32 | INIFile("myapp.ini"), 33 | Environment(prefix="MYAPP_"), 34 | Commandline()) 35 | 36 | # Prints "Hello world!", i.e the value provided by command-line 37 | # arguments. Latter sources take precedence over earlier sources. 38 | print("Hello %s!" % config.hello) 39 | 40 | * A flexible system makes it possible to specify the sources of 41 | configuration information, including which source takes 42 | precedence. Implementations of many common sources are included and 43 | there is a API for writing new ones. 44 | * Included configuration sources for INI files, YAML files, JSON file, 45 | PList files, etcd stores (read-write), Python source files, 46 | hard-coded defaults, command line options, environment variables 47 | (read-only). 48 | * Configuration can include subsections 49 | (ie. ``config.downloading.refresh``) and if a 50 | subsection does not contain a requested setting, it can optionally 51 | be fetched from the main configuration (if ``config.module.retry`` 52 | is missing, ``config.retry`` can be used instead). 53 | * Configuration settings can be changed by your code (i.e. to update a 54 | "lastmodified" setting or similar), and changes can be persisted 55 | (saved) to the backend of your choice. 56 | * Configuration settings are typed (ie. if a setting should contain a 57 | date, it's made available to your code as a 58 | ``datetime.date`` object, not a ``str``). If 59 | settings are fetched from backends that do not themselves provide 60 | typed data (ie. environment variables, which by themselves are 61 | strings only), a system for type coercion makes it possible to 62 | specify how data should be converted. 63 | 64 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: false 2 | version: 0.3.4.dev1.{build} 3 | environment: 4 | matrix: 5 | - PYTHON: "C:/Python27" 6 | - PYTHON: "C:/Python34" 7 | init: 8 | - ps: Invoke-WebRequest "https://bootstrap.pypa.io/get-pip.py" -OutFile "c:/get-pip.py" 9 | - ps: "git config --global core.autocrlf false" # always use unix lineendings 10 | install: 11 | - "%PYTHON%/python.exe c:/get-pip.py" 12 | - "%PYTHON%/Scripts/pip.exe -q install -r requirements.txt" 13 | test_script: 14 | - "%PYTHON%/python.exe setup.py test" 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/complexity.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/complexity.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/complexity" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/complexity" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." -------------------------------------------------------------------------------- /docs/RELEASE.rst: -------------------------------------------------------------------------------- 1 | Internal notes on how to do a release 2 | ===================================== 3 | 4 | $ git checkout -b release/0.1.0 5 | # update version info in layeredconfig/__init__.py 6 | $ git commit -am "Final release prep" 7 | # push changes so that travis-ci/appveyor can test the bits to be released 8 | $ git push --set-upstream origin release/0.1.0 9 | # tag release and push to github, to make this tree be a "release" 10 | $ git tag -a "v0.1.0" -m "Initial release" 11 | $ git push --tags # makes the release show up in Github 12 | # register a new version on pypi and upload it 13 | $ python setup.py register 14 | $ python setup.py sdist 15 | $ python setup.py bdist_wheel --universal 16 | $ twine upload dist/layeredconfig-0.1.0.tar.gz dist/layeredconfig-0.1.0-py2.py3-none-any.whl 17 | # start the next cycle 18 | $ git checkout master 19 | $ git merge release/0.1.0 20 | # update layeredconfig/__init__.py to eg version=0.1.1.dev1 and ideally also appveyor.yml 21 | $ git commit -m "start of next iteration" layeredconfig/__init__.py 22 | $ git push 23 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API reference 2 | ============= 3 | 4 | .. autoclass:: layeredconfig.LayeredConfig 5 | :members: 6 | :undoc-members: 7 | :member-order: bysource 8 | -------------------------------------------------------------------------------- /docs/argparse.rst: -------------------------------------------------------------------------------- 1 | Using LayeredConfig with ``argparse`` 2 | ===================================== 3 | 4 | The standard module for handling command line arguments in python is 5 | :py:mod:`argparse`. This module handles much of the same things as 6 | LayeredConfig does (eg. defining the default values and types of 7 | arguments and making them easily accessed), but it isn't able to read 8 | parameter values from other sources such as INI files or environment 9 | variables. 10 | 11 | LayeredConfig integrates with argparse through the 12 | :py:class:`~layeredconfig.Commandline` config source. If you have 13 | existing code to set up an :py:class:`argparse.ArgumentParser` object, 14 | you can re-use that with LayeredConfig. 15 | 16 | .. literalinclude:: examples/argparse-example.py 17 | :start-after: # begin import 18 | :end-before: # end import 19 | 20 | After this setup, you might want to create any number of config 21 | sources. In this example we use a :py:class:`~layeredconfig.Defaults` 22 | object, mostly used for specifying the type of different arguments. 23 | 24 | .. literalinclude:: examples/argparse-example.py 25 | :start-after: # begin defaults 26 | :end-before: # end defaults 27 | 28 | And also an :py:class:`~layeredconfig.INIFile` that is used to store 29 | actual values for most parameters. 30 | 31 | .. literalinclude:: examples/argparse-example.py 32 | :start-after: # begin inifile 33 | :end-before: # end inifile 34 | 35 | Next up, we create an instance of :py:class:`argparse.ArgumentParser` 36 | in the normal way. Note that in this example, we specify the types of 37 | some of the parameters, since this is representative of how 38 | ArgumentParser normally is used. But you can also omit this 39 | information (the ``action`` and ``type`` parameters to 40 | :py:meth:`~argparse.ArgumentParser.add_argument`) as long as you 41 | provide information through a Defaults config source object. 42 | 43 | Note: we don't add arguments for ``--duedate`` or ``--submodule-lastrun`` to 44 | show that LayeredConfig can define these arguments based on other 45 | sources. Also note that defaults values are automatically fetched from 46 | either defaults or inifile. 47 | 48 | .. literalinclude:: examples/argparse-example.py 49 | :start-after: # begin argparse 50 | :end-before: # end argparse 51 | 52 | Now, instead of calling 53 | :py:meth:`~argparse.ArgumentParser.parse_args`, you can pass this 54 | initialized parser object as a named parameter when creating a 55 | :py:class:`~layeredconfig.Commandline` source, and use this to create 56 | a :py:class:`~layeredconfig.LayeredConfig` object. 57 | 58 | Note that you can use short parameters if you want, as long as you 59 | define long parameters (that map to your other parameter names) as 60 | well 61 | 62 | .. literalinclude:: examples/argparse-example.py 63 | :start-after: # begin layeredconfig 64 | :end-before: # end layeredconfig 65 | 66 | The standard feature of argparse to create a help text if the ``-h`` 67 | parameter is given still exists. Note that it will also display 68 | parameters such as `--name``, which was defined in the 69 | :py:class:`~layeredconfig.Defaults` object, not in the parser object. 70 | 71 | .. literalinclude:: examples/argparse-example.py 72 | :start-after: # begin showhelp 73 | :end-before: # end showhelp 74 | 75 | 76 | .. warning:: 77 | 78 | Using a bespoke :py:class:`argparse.ArgumentParser` together with 79 | subsections is a bit more complicated. If you want to do that, you 80 | will need to setup each argument to the ArgumenParser object by 81 | explicitly naming the internal name for the attribute as specifid 82 | by the `dest` parameter, and separating the subsections with the 83 | special :py:data:`layeredconfig.UNIT_SEP` delimiter, eg: 84 | 85 | .. literalinclude:: examples/argparse-example.py 86 | :start-after: # begin subsection 87 | :end-before: # end subsection 88 | 89 | 90 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # complexity documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Jul 9 22:26:36 2013. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | 19 | # If extensions (or modules to document with autodoc) are in another 20 | # directory, add these directories to sys.path here. If the directory is 21 | # relative to the documentation root, use os.path.abspath to make it 22 | # absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # Get the project root dir, which is the parent dir of this 26 | cwd = os.getcwd() 27 | project_root = os.path.dirname(cwd) 28 | 29 | # Insert the project root dir as the first element in the PYTHONPATH. 30 | # This lets us ensure that the source package is imported, and that its 31 | # version is used. 32 | sys.path.insert(0, project_root) 33 | 34 | import layeredconfig 35 | 36 | # -- General configuration --------------------------------------------- 37 | 38 | # If your documentation needs a minimal Sphinx version, state it here. 39 | #needs_sphinx = '1.0' 40 | 41 | # Add any Sphinx extension module names here, as strings. They can be 42 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 43 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ['_templates'] 47 | 48 | # The suffix of source filenames. 49 | source_suffix = '.rst' 50 | 51 | # The encoding of source files. 52 | #source_encoding = 'utf-8-sig' 53 | 54 | # The master toctree document. 55 | master_doc = 'index' 56 | 57 | # General information about the project. 58 | project = u'LayeredConfig' 59 | copyright = u'2014, Staffan Malmgren' 60 | 61 | # The version info for the project you're documenting, acts as replacement 62 | # for |version| and |release|, also used in various other places throughout 63 | # the built documents. 64 | # 65 | # The short X.Y version. 66 | version = layeredconfig.__version__ 67 | # The full version, including alpha/beta/rc tags. 68 | release = layeredconfig.__version__ 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | #language = None 73 | 74 | # There are two options for replacing |today|: either, you set today to 75 | # some non-false value, then it is used: 76 | #today = '' 77 | # Else, today_fmt is used as the format for a strftime call. 78 | #today_fmt = '%B %d, %Y' 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | exclude_patterns = ['_build'] 83 | 84 | # The reST default role (used for this markup: `text`) to use for all 85 | # documents. 86 | #default_role = None 87 | 88 | # If true, '()' will be appended to :func: etc. cross-reference text. 89 | #add_function_parentheses = True 90 | 91 | # If true, the current module name will be prepended to all description 92 | # unit titles (such as .. function::). 93 | #add_module_names = True 94 | 95 | # If true, sectionauthor and moduleauthor directives will be shown in the 96 | # output. They are ignored by default. 97 | #show_authors = False 98 | 99 | # The name of the Pygments (syntax highlighting) style to use. 100 | pygments_style = 'sphinx' 101 | 102 | # A list of ignored prefixes for module index sorting. 103 | #modindex_common_prefix = [] 104 | 105 | # If true, keep warnings as "system message" paragraphs in the built 106 | # documents. 107 | #keep_warnings = False 108 | 109 | 110 | # -- Options for HTML output ------------------------------------------- 111 | 112 | # The theme to use for HTML and HTML Help pages. See the documentation for 113 | # a list of builtin themes. 114 | html_theme = 'default' 115 | 116 | # Theme options are theme-specific and customize the look and feel of a 117 | # theme further. For a list of options available for each theme, see the 118 | # documentation. 119 | #html_theme_options = {} 120 | 121 | # Add any paths that contain custom themes here, relative to this directory. 122 | #html_theme_path = [] 123 | 124 | # The name for this set of Sphinx documents. If None, it defaults to 125 | # " v documentation". 126 | #html_title = None 127 | 128 | # A shorter title for the navigation bar. Default is the same as 129 | # html_title. 130 | #html_short_title = None 131 | 132 | # The name of an image file (relative to this directory) to place at the 133 | # top of the sidebar. 134 | #html_logo = None 135 | 136 | # The name of an image file (within the static path) to use as favicon 137 | # of the docs. This file should be a Windows icon file (.ico) being 138 | # 16x16 or 32x32 pixels large. 139 | #html_favicon = None 140 | 141 | # Add any paths that contain custom static files (such as style sheets) 142 | # here, relative to this directory. They are copied after the builtin 143 | # static files, so a file named "default.css" will overwrite the builtin 144 | # "default.css". 145 | html_static_path = ['_static'] 146 | 147 | # If not '', a 'Last updated on:' timestamp is inserted at every page 148 | # bottom, using the given strftime format. 149 | #html_last_updated_fmt = '%b %d, %Y' 150 | 151 | # If true, SmartyPants will be used to convert quotes and dashes to 152 | # typographically correct entities. 153 | #html_use_smartypants = True 154 | 155 | # Custom sidebar templates, maps document names to template names. 156 | #html_sidebars = {} 157 | 158 | # Additional templates that should be rendered to pages, maps page names 159 | # to template names. 160 | #html_additional_pages = {} 161 | 162 | # If false, no module index is generated. 163 | #html_domain_indices = True 164 | 165 | # If false, no index is generated. 166 | #html_use_index = True 167 | 168 | # If true, the index is split into individual pages for each letter. 169 | #html_split_index = False 170 | 171 | # If true, links to the reST sources are added to the pages. 172 | #html_show_sourcelink = True 173 | 174 | # If true, "Created using Sphinx" is shown in the HTML footer. 175 | # Default is True. 176 | #html_show_sphinx = True 177 | 178 | # If true, "(C) Copyright ..." is shown in the HTML footer. 179 | # Default is True. 180 | #html_show_copyright = True 181 | 182 | # If true, an OpenSearch description file will be output, and all pages 183 | # will contain a tag referring to it. The value of this option 184 | # must be the base URL from which the finished HTML is served. 185 | #html_use_opensearch = '' 186 | 187 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 188 | #html_file_suffix = None 189 | 190 | # Output file base name for HTML help builder. 191 | htmlhelp_basename = 'layeredconfigdoc' 192 | 193 | 194 | # -- Options for LaTeX output ------------------------------------------ 195 | 196 | latex_elements = { 197 | # The paper size ('letterpaper' or 'a4paper'). 198 | #'papersize': 'letterpaper', 199 | 200 | # The font size ('10pt', '11pt' or '12pt'). 201 | #'pointsize': '10pt', 202 | 203 | # Additional stuff for the LaTeX preamble. 204 | #'preamble': '', 205 | } 206 | 207 | # Grouping the document tree into LaTeX files. List of tuples 208 | # (source start file, target name, title, author, documentclass 209 | # [howto/manual]). 210 | latex_documents = [ 211 | ('index', 'layeredconfig.tex', 212 | u'LayeredConfig Documentation', 213 | u'Staffan Malmgren', 'manual'), 214 | ] 215 | 216 | # The name of an image file (relative to this directory) to place at 217 | # the top of the title page. 218 | #latex_logo = None 219 | 220 | # For "manual" documents, if this is true, then toplevel headings 221 | # are parts, not chapters. 222 | #latex_use_parts = False 223 | 224 | # If true, show page references after internal links. 225 | #latex_show_pagerefs = False 226 | 227 | # If true, show URL addresses after external links. 228 | #latex_show_urls = False 229 | 230 | # Documents to append as an appendix to all manuals. 231 | #latex_appendices = [] 232 | 233 | # If false, no module index is generated. 234 | #latex_domain_indices = True 235 | 236 | 237 | # -- Options for manual page output ------------------------------------ 238 | 239 | # One entry per manual page. List of tuples 240 | # (source start file, name, description, authors, manual section). 241 | man_pages = [ 242 | ('index', 'layeredconfig', 243 | u'LayeredConfig Documentation', 244 | [u'Staffan Malmgren'], 1) 245 | ] 246 | 247 | # If true, show URL addresses after external links. 248 | #man_show_urls = False 249 | 250 | 251 | # -- Options for Texinfo output ---------------------------------------- 252 | 253 | # Grouping the document tree into Texinfo files. List of tuples 254 | # (source start file, target name, title, author, 255 | # dir menu entry, description, category) 256 | texinfo_documents = [ 257 | ('index', 'layeredconfig', 258 | u'LayeredConfig Documentation', 259 | u'Staffan Malmgren', 260 | 'layeredconfig', 261 | 'One line description of project.', 262 | 'Miscellaneous'), 263 | ] 264 | 265 | # Documents to append as an appendix to all manuals. 266 | #texinfo_appendices = [] 267 | 268 | # If false, no module index is generated. 269 | #texinfo_domain_indices = True 270 | 271 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 272 | #texinfo_show_urls = 'footnote' 273 | 274 | # If true, do not generate a @detailmenu in the "Top" node's menu. 275 | #texinfo_no_detailmenu = False 276 | 277 | intersphinx_mapping = {'python': ('http://docs.python.org/3/', None), 278 | } 279 | 280 | autoclass_content = "both" 281 | 282 | # http://stackoverflow.com/a/10870416 -- just document class level 283 | # variables by name and docstring, not their default value 284 | from sphinx.ext.autodoc import ModuleLevelDocumenter, DataDocumenter 285 | 286 | def add_directive_header(self,sig): 287 | ModuleLevelDocumenter.add_directive_header(self,sig) 288 | # omit the rest 289 | DataDocumenter.add_directive_header = add_directive_header 290 | -------------------------------------------------------------------------------- /docs/configsource.rst: -------------------------------------------------------------------------------- 1 | Implementing custom ConfigSource classes 2 | ======================================== 3 | 4 | If you want to get configuration settings from some other sources than 5 | the built-in sources, you should create a class that derives from 6 | :py:class:`~layeredconfig.ConfigSource` and implement a few 7 | methods. 8 | 9 | If your chosen source can expose the settings as a (possibly nested) 10 | :py:class:`dict`, it might be easier to derive from 11 | :py:class:`~layeredconfig.DictSource` which already provide 12 | implementations of many methods. 13 | 14 | .. autoclass:: layeredconfig.ConfigSource 15 | :members: 16 | :undoc-members: 17 | :member-order: bysource 18 | 19 | 20 | .. autoclass:: layeredconfig.DictSource 21 | :members: 22 | :member-order: bysource 23 | -------------------------------------------------------------------------------- /docs/examples/argparse-example.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | # begin import 4 | import sys 5 | import argparse 6 | from datetime import date, datetime 7 | from layeredconfig import LayeredConfig, Defaults, INIFile, Commandline, UNIT_SEP 8 | # end import 9 | 10 | 11 | # begin defaults 12 | defaults = Defaults({'home': str, 13 | 'name': 'MyApp', 14 | 'dostuff': bool, 15 | 'times': int, 16 | 'duedate': date, 17 | 'things': list, 18 | 'submodule': {'retry': bool, 19 | 'lastrun': datetime 20 | } 21 | }) 22 | # end defaults 23 | 24 | # begin inifile 25 | with open("myapp.ini", "w") as fp: 26 | fp.write("""[__root__] 27 | home = /tmp/myapp 28 | dostuff = False 29 | times = 4 30 | duedate = 2014-10-30 31 | things = Huey, Dewey, Louie 32 | 33 | [submodule] 34 | retry = False 35 | lastrun = 2014-10-30 16:40:22 36 | """) 37 | inifile = INIFile("myapp.ini") 38 | # end inifile 39 | 40 | # begin argparse 41 | parser = argparse.ArgumentParser("This is a simple program") 42 | parser.add_argument("--home", help="The home directory of the app") 43 | parser.add_argument('--dostuff', action="store_true", help="Do some work") 44 | parser.add_argument("-t", "--times", type=int, help="Number of times to do it") 45 | parser.add_argument('--things', action="append", help="Extra things to crunch") 46 | parser.add_argument('--retry', action="store_true", help="Try again") 47 | parser.add_argument("file", metavar="FILE", help="The filename to process") 48 | # end argparse 49 | 50 | # begin layeredconfig 51 | sys.argv = ['./myapp.py', '--home=/opt/myapp', '-t=2', '--dostuff', 'file.txt'] 52 | cfg = LayeredConfig(defaults, 53 | inifile, 54 | Commandline(parser=parser)) 55 | print("Starting %s in %s for %r times (doing work: %s)" % (cfg.name, 56 | cfg.home, 57 | cfg.times, 58 | cfg.dostuff)) 59 | # should print "Starting MyApp in /opt/myapp for 2 times (doing work: True)" 60 | # end layeredconfig 61 | 62 | # begin showhelp 63 | sys.argv = ['./myapp.py', '-h'] 64 | cfg = LayeredConfig(defaults, 65 | inifile, 66 | Commandline(parser=parser)) 67 | # end showhelp 68 | 69 | # begin nodefaults 70 | 71 | # NOTE: we never reach this because the previous call to -h will have 72 | # called sys.exit 73 | 74 | cfg = LayeredConfig(inifile, 75 | Commandline(parser=parser)) 76 | # note that cfg.times is now a str, not an int 77 | print("Starting in %s for %r times" % (cfg.home, cfg.times)) 78 | # end nodefaults 79 | 80 | # begin subsection 81 | parser.add_argument("--submodule-retry", help="Whether to retry the submodule", 82 | dest="submodule"+UNIT_SEP+"retry") 83 | # end subsection 84 | -------------------------------------------------------------------------------- /docs/examples/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import os 4 | import multiprocessing 5 | from datetime import datetime, date 6 | 7 | home = os.getcwd() 8 | name = 'My App' 9 | dostuff = name.istitle() 10 | duedate = date.today() 11 | submodule = Subsection() 12 | submodule.retry = True 13 | -------------------------------------------------------------------------------- /docs/examples/defaults.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime 2 | 3 | home = '/tmp/myapp' 4 | name = 'MyApp' 5 | dostuff = False 6 | times = 4 7 | duedate = date(2014, 10, 30), 8 | things = ['Huey', 'Dewey', 'Louie'] 9 | submodule = Subsection() 10 | submodule.retry = False 11 | submodule.lastrun = datetime(2014, 10, 30, 16, 40, 22) 12 | -------------------------------------------------------------------------------- /docs/examples/firststep.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | # begin firststep 3 | from layeredconfig import (LayeredConfig, Defaults, INIFile, 4 | Environment, Commandline) 5 | 6 | # This represents four different way of specifying the value of the 7 | # configuration option "hello": 8 | 9 | # 1. hard-coded defaults 10 | defaults = {"hello": "is it me you're looking for?"} 11 | 12 | # 2. INI configuration file 13 | with open("myapp.ini", "w") as fp: 14 | fp.write(""" 15 | [__root__] 16 | hello = kitty 17 | """) 18 | 19 | # 3. enironment variables 20 | import os 21 | os.environ['MYAPP_HELLO'] = 'goodbye' 22 | 23 | # 4.command-line arguments 24 | import sys 25 | sys.argv = ['./myapp.py', '--hello=world'] 26 | 27 | # Create a config object that gets settings from these four 28 | # sources. 29 | config = LayeredConfig(Defaults(defaults), 30 | INIFile("myapp.ini"), 31 | Environment(prefix="MYAPP_"), 32 | Commandline()) 33 | 34 | # Prints "Hello world!", i.e the value provided by command-line 35 | # arguments. Latter sources take precedence over earlier sources. 36 | print("Hello %s!" % config.hello) 37 | # end firststep 38 | 39 | return_value = True 40 | -------------------------------------------------------------------------------- /docs/examples/myapp.ini: -------------------------------------------------------------------------------- 1 | [__root__] 2 | home = /usr/home/staffan/.myapp 3 | 4 | [submodule] 5 | retry = True 6 | lastrun = 2014-10-31 16:40:22 7 | -------------------------------------------------------------------------------- /docs/examples/pyfile-example.py: -------------------------------------------------------------------------------- 1 | # begin example 2 | from layeredconfig import LayeredConfig, PyFile, Defaults 3 | from datetime import date, datetime 4 | 5 | conf = LayeredConfig(Defaults({'home': '/tmp/myapp', 6 | 'name': 'MyApp', 7 | 'dostuff': False, 8 | 'times': 4, 9 | 'duedate': date(2014, 10, 30), 10 | 'things': ['Huey', 'Dewey', 'Louie'], 11 | 'submodule': { 12 | 'retry': False, 13 | 'lastrun': datetime(2014, 10, 30, 16, 40, 22) 14 | } 15 | }), 16 | PyFile("conf.py")) 17 | # end example 18 | from datetime import date, datetime 19 | import os 20 | assert conf.home == os.getcwd() 21 | assert conf.name == 'My App' 22 | assert conf.dostuff is True 23 | assert conf.times == 4 24 | assert conf.duedate == date.today() 25 | assert conf.things == ['Huey', 'Dewey', 'Louie'] 26 | assert conf.submodule.lastrun == datetime(2014, 10, 30, 16, 40, 22) 27 | assert conf.submodule.retry is True 28 | 29 | return_value = conf.name 30 | -------------------------------------------------------------------------------- /docs/examples/pyfile-example2.py: -------------------------------------------------------------------------------- 1 | from layeredconfig import LayeredConfig, PyFile 2 | 3 | # begin example 4 | conf = LayeredConfig(PyFile("defaults.py"), 5 | PyFile("conf.py")) 6 | # end example 7 | from datetime import date, datetime 8 | import os 9 | assert conf.home == os.getcwd() 10 | assert conf.name == 'My App' 11 | assert conf.dostuff is True 12 | assert conf.times == 4 13 | assert conf.duedate == date.today() 14 | assert conf.things == ['Huey', 'Dewey', 'Louie'] 15 | assert conf.submodule.lastrun == datetime(2014, 10, 30, 16, 40, 22) 16 | assert conf.submodule.retry is True 17 | 18 | return_value = conf.name 19 | -------------------------------------------------------------------------------- /docs/examples/usage.py: -------------------------------------------------------------------------------- 1 | # begin import-1 2 | from __future__ import print_function 3 | from layeredconfig import LayeredConfig 4 | # end import-1 5 | 6 | # begin import-2 7 | from layeredconfig import Defaults, INIFile, Environment, Commandline 8 | # end import-2 9 | 10 | # begin defaults 11 | from datetime import date, datetime 12 | mydefaults = Defaults({'home': '/tmp/myapp', 13 | 'name': 'MyApp', 14 | 'dostuff': False, 15 | 'times': 4, 16 | 'duedate': date(2014, 10, 30), 17 | 'things': ['Huey', 'Dewey', 'Louie'], 18 | 'submodule': { 19 | 'retry': False, 20 | 'lastrun': datetime(2014, 10, 30, 16, 40, 22) 21 | } 22 | }) 23 | # end defaults 24 | 25 | # begin inifile 26 | myinifile = INIFile("myapp.ini") 27 | # end inifile 28 | 29 | # begin environment 30 | env = {'MYAPP_HOME': 'C:\\Progra~1\\MyApp', 31 | 'MYAPP_SUBMODULE_RETRY': 'True'} 32 | myenv = Environment(env, prefix="MYAPP_") 33 | # end environment 34 | 35 | # begin commandline 36 | mycmdline = Commandline(['-f', '--home=/opt/myapp', '--times=2', '--dostuff']) 37 | rest = mycmdline.rest 38 | # end commandline 39 | assert rest == ['-f'] 40 | 41 | # begin makeconfig 42 | cfg = LayeredConfig(mydefaults, 43 | myinifile, 44 | myenv, 45 | mycmdline) 46 | # end makeconfig 47 | import os 48 | def do_stuff(action, idx): 49 | pass 50 | 51 | # begin useconfig 52 | print("%s starting, home in %s" % (cfg.name, cfg.home)) 53 | # end useconfig 54 | 55 | # begin usetyping 56 | delay = date.today() - cfg.duedate # date 57 | if cfg.dostuff: # bool 58 | for i in range(cfg.times): # int 59 | print(", ".join(cfg.things)) # list 60 | # end usetyping 61 | 62 | # begin usesubconfig 63 | subcfg = cfg.submodule 64 | if subcfg.retry: 65 | print(subcfg.lastrun.isoformat()) 66 | # end usesubconfig 67 | 68 | try: 69 | print(subcfg.home) 70 | except AttributeError: 71 | pass 72 | 73 | # begin usecascade 74 | cfg = LayeredConfig(mydefaults, myinifile, myenv, mycmdline, cascade=True) 75 | subcfg = cfg.submodule 76 | print(subcfg.home) # prints '/opt/myapp', from Commandline source root section 77 | # end usecascade 78 | assert subcfg.home == '/opt/myapp' 79 | 80 | # begin writeconfig 81 | subcfg.lastrun = datetime.now() 82 | LayeredConfig.write(cfg) 83 | # end writeconfig 84 | 85 | return_value = True 86 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. complexity documentation master file, created by 2 | sphinx-quickstart on Tue Jul 9 22:26:36 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | LayeredConfig 7 | ============= 8 | 9 | LayeredConfig compiles configuration from files, environment 10 | variables, command line arguments, hard-coded default values, or other 11 | backends, and makes it available to your code in a simple way. 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | readme 17 | usage 18 | argparse 19 | pyfile 20 | api 21 | sources 22 | configsource 23 | -------------------------------------------------------------------------------- /docs/layeredconfig.rst: -------------------------------------------------------------------------------- 1 | layeredconfig package 2 | ===================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | layeredconfig.commandline module 8 | -------------------------------- 9 | 10 | .. automodule:: layeredconfig.commandline 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | layeredconfig.configsource module 16 | --------------------------------- 17 | 18 | .. automodule:: layeredconfig.configsource 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | layeredconfig.defaults module 24 | ----------------------------- 25 | 26 | .. automodule:: layeredconfig.defaults 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | layeredconfig.dictsource module 32 | ------------------------------- 33 | 34 | .. automodule:: layeredconfig.dictsource 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | layeredconfig.environment module 40 | -------------------------------- 41 | 42 | .. automodule:: layeredconfig.environment 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | layeredconfig.etcdstore module 48 | ------------------------------ 49 | 50 | .. automodule:: layeredconfig.etcdstore 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | layeredconfig.inifile module 56 | ---------------------------- 57 | 58 | .. automodule:: layeredconfig.inifile 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | layeredconfig.jsonfile module 64 | ----------------------------- 65 | 66 | .. automodule:: layeredconfig.jsonfile 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | layeredconfig.layeredconfig module 72 | ---------------------------------- 73 | 74 | .. automodule:: layeredconfig.layeredconfig 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | layeredconfig.plistfile module 80 | ------------------------------ 81 | 82 | .. automodule:: layeredconfig.plistfile 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | layeredconfig.pyfile module 88 | --------------------------- 89 | 90 | .. automodule:: layeredconfig.pyfile 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | layeredconfig.yamlfile module 96 | ----------------------------- 97 | 98 | .. automodule:: layeredconfig.yamlfile 99 | :members: 100 | :undoc-members: 101 | :show-inheritance: 102 | 103 | 104 | Module contents 105 | --------------- 106 | 107 | .. automodule:: layeredconfig 108 | :members: 109 | :undoc-members: 110 | :show-inheritance: 111 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\complexity.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\complexity.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end -------------------------------------------------------------------------------- /docs/pyfile.rst: -------------------------------------------------------------------------------- 1 | Embedding configuration in python files 2 | ======================================= 3 | 4 | In many cases, it's desirable to let the end user specify 5 | configuration in the same langauge as the rest of the system (`Django 6 | `_ and `Sphinx 7 | `_ are examples of frameworks that works this 8 | way). LayeredConfig provides the source :py:class:`~layeredconfig.PyFile` 9 | that lets the user create configuration using normal python code. 10 | 11 | If you create a file like ``conf.py`` with the following contents: 12 | 13 | .. literalinclude:: examples/conf.py 14 | 15 | .. note:: 16 | 17 | The class ``Subsection`` will automatically be imported into 18 | ``conf.py`` and is used to create new subsections. Parameters in 19 | subsections are created as normal attributes on the subsection 20 | object. 21 | 22 | And load it, together with a :py:class:`~layeredconfig.Defaults` 23 | source like in previous examples: 24 | 25 | .. literalinclude:: examples/pyfile-example.py 26 | :start-after: # begin example 27 | :end-before: # end example 28 | 29 | The configuration object will act the same as in previous examples, 30 | ie. values that are specified in ``conf.py`` be used, and values 31 | specified in the Defaults object only used if ``conf.py`` doesn't 32 | specify them. 33 | 34 | .. note:: 35 | 36 | The :py:class:`~layeredconfig.PyFile` source is read-only, so it 37 | should not be used when it's desirable to be able to save changed 38 | configuration parameters to a file. Use 39 | :py:class:`~layeredconfig.PyFile` or one of the other ``*File`` 40 | sources in these cases. 41 | 42 | It's also possible to keep system defaults in a separate python file, 43 | load these with one :py:class:`~layeredconfig.PyFile` instance, and 44 | then let the user override parts using a separate 45 | :py:class:`~layeredconfig.PyFile` instance. Functionally, this is not 46 | very different than loading system defaults using a 47 | :py:class:`~layeredconfig.Defaults` source, but it might be preferable 48 | in some cases. As an example, if the file ``defaults.py`` contains the 49 | following code: 50 | 51 | .. literalinclude:: examples/defaults.py 52 | 53 | And a LayeredConfig object is initialized in the following way, then 54 | the resulting configuration object works identically to the above: 55 | 56 | .. literalinclude:: examples/pyfile-example2.py 57 | :start-after: # begin example 58 | :end-before: # end example 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | .. include:: ../README.rst 5 | -------------------------------------------------------------------------------- /docs/sources.rst: -------------------------------------------------------------------------------- 1 | Available sources 2 | ================= 3 | 4 | Hardcoded defaults 5 | ------------------ 6 | .. autoclass:: layeredconfig.Defaults 7 | :members: 8 | :member-order: bysource 9 | 10 | Environment variables 11 | --------------------- 12 | 13 | .. autoclass:: layeredconfig.Environment 14 | :members: 15 | :member-order: bysource 16 | 17 | Command-line parameters 18 | ----------------------- 19 | 20 | .. autoclass:: layeredconfig.Commandline 21 | :members: 22 | :member-order: bysource 23 | 24 | INI files 25 | --------- 26 | 27 | .. autoclass:: layeredconfig.INIFile 28 | :members: 29 | :member-order: bysource 30 | 31 | JSON files 32 | ---------- 33 | 34 | .. autoclass:: layeredconfig.JSONFile 35 | :members: 36 | :member-order: bysource 37 | 38 | YAML files 39 | ---------- 40 | 41 | .. autoclass:: layeredconfig.YAMLFile 42 | :members: 43 | :member-order: bysource 44 | 45 | PList files 46 | ----------- 47 | 48 | .. autoclass:: layeredconfig.PListFile 49 | :members: 50 | :member-order: bysource 51 | 52 | Python files 53 | ------------ 54 | 55 | .. autoclass:: layeredconfig.PyFile 56 | :members: 57 | :member-order: bysource 58 | 59 | ``etcd`` stores 60 | --------------- 61 | 62 | .. autoclass:: layeredconfig.EtcdStore 63 | :members: 64 | :member-order: bysource 65 | 66 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | To use LayeredConfig in a project: 5 | 6 | .. literalinclude:: examples/usage.py 7 | :start-after: # begin import-1 8 | :end-before: # end import-1 9 | 10 | Also, import any Configuration sources you want to use. It's common to 11 | have one source for code defaults, one configuration file (INI file in 12 | this example), one using environment variables as source, and one 13 | using command lines: 14 | 15 | .. literalinclude:: examples/usage.py 16 | :start-after: # begin import-2 17 | :end-before: # end import-2 18 | 19 | Each configuration source must be initialized in some way. The 20 | :py:class:`~layeredconfig.Defaults` source takes a :py:class:`dict`, 21 | possibly nested: 22 | 23 | .. literalinclude:: examples/usage.py 24 | :start-after: # begin defaults 25 | :end-before: # end defaults 26 | 27 | A configuration source such as :py:class:`~layeredconfig.INIFile` 28 | takes the name of a file. In this example, we use a INI-style file. 29 | 30 | .. literalinclude:: examples/usage.py 31 | :start-after: # begin inifile 32 | :end-before: # end inifile 33 | 34 | .. note:: 35 | 36 | LayeredConfig uses the :py:mod:`configparser` module, which 37 | requires that each setting is placed within a section. By default, 38 | top-level settings are placed within the ``[__root__]`` section. 39 | 40 | In this example, we assume that there is a file called 41 | ``myapp.ini`` within the current directory with the following 42 | contents: 43 | 44 | .. literalinclude:: examples/myapp.ini 45 | 46 | The :py:class:`~layeredconfig.Environment` source uses environment 47 | variables as settings. Since the entire environment is not suitable to 48 | use as a configuration, use of this source requires that a ``prefix`` 49 | is given. Only environment variables starting with this prefix are 50 | used. Furthermore, since the name of environment variable typically 51 | uses uppercase, they are by default lowercased by this source. This 52 | means that, in this example, the value of the environmentvariable 53 | ``MYAPP_HOME`` will be available as the configuration setting 54 | ``home``. 55 | 56 | .. literalinclude:: examples/usage.py 57 | :start-after: # begin environment 58 | :end-before: # end environment 59 | 60 | Finally, the :py:class:`~layeredconfig.Commandline` processes the 61 | contents of sys.argv and uses any parameter starting with ``--`` as a 62 | setting, such as ``--home=/Users/staffan/Library/MyApp``. Arguments 63 | that do not match this (such as positional arguments or short options 64 | like ``-f``) are made available through the ``rest`` property, to be 65 | used with eg. :py:mod:`argparse`. 66 | 67 | .. literalinclude:: examples/usage.py 68 | :start-after: # begin commandline 69 | :end-before: # end commandline 70 | 71 | Now that we have our config sources all set up, we can create the 72 | actual configuration object: 73 | 74 | .. literalinclude:: examples/usage.py 75 | :start-after: # begin makeconfig 76 | :end-before: # end makeconfig 77 | 78 | And we use the attributes on the config object to access the settings: 79 | 80 | .. literalinclude:: examples/usage.py 81 | :start-after: # begin useconfig 82 | :end-before: # end useconfig 83 | 84 | 85 | .. _precedence: 86 | 87 | Precedence 88 | ---------- 89 | 90 | Since several sources may contain a setting, A simple precedence 91 | system determines which setting is actually used. In the above 92 | example, the printed string is ``"MyApp starting, home in 93 | /opt/myapp"``. This is because while ``name`` was specified only by the 94 | mydefaults source, ``home`` was specified by source with higher 95 | predecence (``mycmdline``). The order of sources passed to 96 | LayeredConfig determines predecence, with the last source having the 97 | highest predecence. 98 | 99 | .. _configsources: 100 | 101 | Config sources 102 | -------------- 103 | 104 | Apart from the sources used above, there are classes for settings 105 | stored in JSON files, YAML files and PList files, as well as `etcd 106 | stores `_. Each source can to 107 | varying extent be configured with different parameters. See 108 | :doc:`sources` for further details. 109 | 110 | You can also use a single source class multiple times, for example to have 111 | one system-wide config file together with a user config file, where 112 | settings in the latter override the former. 113 | 114 | It's possible to write your own 115 | :py:class:`~layeredconfig.ConfigSource`-based class to read (and 116 | possibly write) from any concievable kind of source. 117 | 118 | .. _typing: 119 | 120 | Typing 121 | ------ 122 | 123 | The values retrieved can have many different types -- not just strings. 124 | 125 | .. literalinclude:: examples/usage.py 126 | :start-after: # begin usetyping 127 | :end-before: # end usetyping 128 | 129 | If a particular source doesn't contain intrinsic typing information, 130 | other sources can be used to find out what type a particular setting 131 | should be. LayeredConfig converts the data automatically. 132 | 133 | .. note:: 134 | 135 | strings are always :py:class:`str` objects, (``unicode`` in python 136 | 2). They are never :py:class:`bytes` objects (``str`` in python 2) 137 | 138 | .. _subsection: 139 | 140 | Subsections 141 | ----------- 142 | 143 | It's possible to divide up settings and group them in subsections. 144 | 145 | .. literalinclude:: examples/usage.py 146 | :start-after: # begin usesubconfig 147 | :end-before: # end usesubconfig 148 | 149 | .. _cascading: 150 | 151 | Cascading 152 | --------- 153 | 154 | If a particular setting is not available in a subsection, 155 | LayeredConfig can optionally look for the same setting in parent 156 | sections if the ``cascade`` option is set. 157 | 158 | .. literalinclude:: examples/usage.py 159 | :start-after: # begin usecascade 160 | :end-before: # end usecascade 161 | 162 | .. _modification: 163 | 164 | Modification and persistance 165 | ---------------------------- 166 | 167 | It's possible to change a setting in a config object. It's also 168 | possible to write out the changed settings to a config source 169 | (ie. configuration files) by calling 170 | :py:meth:`~layeredconfig.LayeredConfig.write` 171 | 172 | .. literalinclude:: examples/usage.py 173 | :start-after: # begin writeconfig 174 | :end-before: # end writeconfig 175 | 176 | -------------------------------------------------------------------------------- /layeredconfig/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __author__ = 'Staffan Malmgren' 4 | __email__ = 'staffan.malmgren@gmail.com' 5 | __version__ = "0.3.4.dev1" 6 | 7 | from .layeredconfig import LayeredConfig 8 | from .configsource import ConfigSource 9 | from .dictsource import DictSource 10 | from .defaults import Defaults 11 | from .inifile import INIFile 12 | from .jsonfile import JSONFile 13 | from .commandline import Commandline, UNIT_SEP 14 | from .environment import Environment 15 | from .plistfile import PListFile 16 | from .yamlfile import YAMLFile 17 | from .pyfile import PyFile 18 | from .etcdstore import EtcdStore 19 | -------------------------------------------------------------------------------- /layeredconfig/commandline.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import argparse 3 | 4 | from six import text_type as str 5 | 6 | from . import ConfigSource 7 | 8 | UNIT_SEP = chr(31) 9 | 10 | class Commandline(ConfigSource): 11 | 12 | rest = [] 13 | """The remainder of the command line, containing all parameters that 14 | couldn't be turned into configuration settings. """ 15 | 16 | def __init__(self, 17 | commandline = None, 18 | parser=None, 19 | sectionsep='-', 20 | add_help=True, 21 | **kwargs): 22 | """Load configuration from command line options. Any long-style 23 | parameters are turned into configuration values, and 24 | parameters containing the section separator (by default 25 | ``"-"``) are turned into nested config objects 26 | (i.e. ``--module-parameter=foo`` results in 27 | ``self.module.parameter == "foo"``. 28 | 29 | If an initialized ArgumentParser object is provided, the 30 | defined parameters in that object is used for supporting short 31 | form options (eg. ``'-f'`` instead of ``'--force'``), typing 32 | information and help text. The standards argparse feature of 33 | printing a helpful message when the '-h' option is given is 34 | retained. 35 | 36 | :param commandline: Command line arguments, in list form like 37 | :py:data:`sys.argv`. If not provided, uses 38 | the real :py:data:`sys.argv`. 39 | :type commandline: list 40 | :param parser: An initialized/configured argparse object 41 | :type parser: argparse.ArgumentParser 42 | :param sectionsep: An alternate section separator instead of ``-``. 43 | :type sectionsep: str 44 | :param add_help: Same as for ArgumentParser() 45 | :type add_help: bool 46 | 47 | """ 48 | # internal arguments: 49 | # * sectionkey: eg 'mymodule' or 'submodule_subsubmodule' etc 50 | super(Commandline, self).__init__(**kwargs) 51 | self.sectionsep = sectionsep 52 | self.sectionkey = kwargs.get('sectionkey', '') 53 | if commandline is None: 54 | if kwargs.get("empty"): 55 | self.commandline = [] 56 | else: 57 | self.commandline = sys.argv[1:] 58 | else: 59 | self.commandline = commandline 60 | self.autoargs = kwargs.get('autoargs', {}) 61 | if parser is None: 62 | if kwargs.get('parent'): 63 | # we're a subsection object, we don't need a parser. 64 | self.parser = None 65 | else: 66 | # create a "bootstrapping" argument parser 67 | self.parser = argparse.ArgumentParser() 68 | for arg in self.commandline: 69 | if arg.startswith("--"): 70 | argname = arg.split("=")[0][2:] 71 | if argname not in self.autoargs: 72 | # at this point we don't know anything about 73 | # this argument other than that it exists and 74 | # our bootstrapping argument parser should 75 | # handle it. 76 | # * use nargs='?' to allow arguments with or without 77 | # values. 78 | # * use const=True to treat valueless arguments as 79 | # set bool flags 80 | # * use action='append' to allow a single argument 81 | # (eg --extra) to be used multiple times with values) 82 | self.parser.add_argument("--%s" % argname, 83 | action='append', 84 | nargs='?', 85 | dest=argname.replace(self.sectionsep, UNIT_SEP), 86 | const=True) 87 | self.autoargs[argname] = True 88 | self._provided_parser = False 89 | else: 90 | self.parser = parser 91 | self._provided_parser = kwargs.get('provided_parser', True) 92 | self.writable = False 93 | # create a (possibly temporary) source (argparse.Namespace object) 94 | # unless we're a subsection object and already have been passed one. 95 | if kwargs.get('parent'): 96 | self.source = None # or {} ? 97 | else: 98 | if kwargs.get('source'): 99 | self.source = kwargs['source'] 100 | else: 101 | self.source, self.rest = self.parser.parse_known_args(self.commandline) 102 | if self._provided_parser and self.rest: 103 | # reconfigure our provided parser and try to add arguments 104 | # for every unprocessed long option. 105 | for arg in self.rest: 106 | if arg.startswith("--"): 107 | argname = arg.split("=")[0][2:] 108 | if (argname not in self.autoargs and 109 | argname not in self.source): 110 | self.parser.add_argument("--%s" % argname, 111 | action='append', 112 | nargs='?', 113 | dest=argname.replace(self.sectionsep, UNIT_SEP), 114 | const=True) 115 | self.autoargs[argname] = True 116 | # now redo the parsing 117 | self.source, self.rest = self.parser.parse_known_args(self.commandline) 118 | 119 | def setup(self, config): 120 | if not self.parser: 121 | # we're in an empty subsection object 122 | return 123 | for key in config: 124 | # since this will be used to handle -h, we need to fill it 125 | # with helpy things (default types, default values, help strings) 126 | try: 127 | currentval = getattr(config, key) 128 | if currentval: 129 | currenttype = type(currentval) 130 | # select a good converter for string -> type 131 | kwargs = {'type': currenttype} 132 | else: 133 | kwargs = {} 134 | 135 | if key not in self.source: 136 | self.parser.add_argument('--%s' % key, 137 | action='append', 138 | nargs='?', 139 | const=True, 140 | dest=key.replace(self.sectionsep, UNIT_SEP), 141 | **kwargs) 142 | self.autoargs[key] = True 143 | except argparse.ArgumentError: 144 | # the parser already had this argument -- assume it's 145 | # fully configured with typing, help, and 146 | # everything. But it'd be nice if we could jam a 147 | # default value in there somehow 148 | pass 149 | # process everything and print help if -h is given 150 | self.source, self.rest = self.parser.parse_known_args(self.commandline) 151 | 152 | def keys(self): 153 | if self.source: 154 | for arg in vars(self.source).keys(): 155 | if arg.startswith(self.sectionkey) and getattr(self.source, arg) is not None: 156 | k = arg[len(self.sectionkey):] 157 | if k.startswith(UNIT_SEP): 158 | k = k[1:] 159 | if UNIT_SEP not in k and getattr(self.source, arg) is not None: 160 | yield k 161 | 162 | def has(self, key): 163 | if self.sectionkey: 164 | key = self.sectionkey + UNIT_SEP + key 165 | try: 166 | return getattr(self.source, key) is not None 167 | except AttributeError: 168 | return False 169 | 170 | def get(self, key): 171 | if self.sectionkey: 172 | key = self.sectionkey + UNIT_SEP + key 173 | r = getattr(self.source, key) 174 | # undo the automatic list behaviour for autodiscovered 175 | # arguments (which has store='append') 176 | key = key.replace(UNIT_SEP, self.sectionsep) 177 | if (key in self.autoargs and 178 | isinstance(r, list) and len(r) == 1): 179 | return r[0] 180 | else: 181 | return r 182 | 183 | def subsections(self): 184 | # argparse has no internal concept of subsections. We 185 | # construct one using arguments like '--mymodule-force'. These 186 | # are transformed into attributes like 'mymoduleforce' on an 187 | # argparse.Namespace object. Therefore, we can create a list 188 | # of subsections 189 | yielded = set() 190 | if self.source: 191 | subsectionsource = dict(self.source._get_kwargs()) 192 | else: 193 | subsectionsource = {} 194 | for args in subsectionsource.keys(): 195 | if args.startswith(self.sectionkey): 196 | args = args[len(self.sectionkey):] 197 | if args.startswith(UNIT_SEP): # sectionsep 198 | args = args[1:] 199 | else: 200 | continue 201 | # '_' is the hardcoded section separator once parse_args 202 | # has transformed the command line args into properties on 203 | # a Namespace object. 204 | if UNIT_SEP in args: 205 | section = args.split(UNIT_SEP)[0] 206 | if section not in yielded: 207 | yield(section) 208 | yielded.add(section) 209 | 210 | def subsection(self, key): 211 | if self.sectionkey: 212 | key = self.sectionkey + UNIT_SEP + key 213 | 214 | return Commandline(self.commandline, 215 | sectionsep=self.sectionsep, 216 | source=self.source, 217 | parser=self.parser, 218 | provided_parser=self._provided_parser, 219 | autoargs=self.autoargs, 220 | sectionkey=key) 221 | 222 | def set(self, key, value): 223 | setattr(self.source, key, value) 224 | 225 | def typed(self, key): 226 | # if the config value has a non-string type, then it's typed 227 | if self.has(key) and not isinstance(self.get(key), str): 228 | return True 229 | 230 | if self._provided_parser: 231 | # a provided parser (not a bootstrapped parser) should be 232 | # able to convert input to typed data -- but only for 233 | # those arguments that were configured 234 | return self.has(key) and key not in self.autoargs 235 | else: 236 | # a boostrapped parser will support typing for bool 237 | # (valueless args) and lists (multiple args) 238 | return self.has(key) and not isinstance(self.get(key), str) 239 | 240 | 241 | -------------------------------------------------------------------------------- /layeredconfig/configsource.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from datetime import date, datetime 3 | import ast 4 | import inspect 5 | 6 | from . import LayeredConfig 7 | 8 | class ConfigSource(object): 9 | __metaclass__ = ABCMeta 10 | 11 | identifier = None 12 | """A string identifying this source, primarily used with 13 | :py:meth:`LayeredConfig.set`.""" 14 | 15 | writable = False 16 | """Whether or not this source can accept changed configuration 17 | settings and store them in the same place as the original setting came 18 | from.""" 19 | 20 | dirty = False 21 | """For writable sources, whether any parameter value in this source 22 | has been changed so that a call to :py:meth:`save` might be needed.""" 23 | 24 | parent = None 25 | """The parent of this source, if this represents a nested 26 | configuration source, or None""" 27 | 28 | source = None 29 | """By convention, this should be your main connection handle, data 30 | access object, or other resource neededed to retrieve the 31 | settings.""" 32 | 33 | @abstractmethod # but subclasses should still call it through super() 34 | def __init__(self, **kwargs): 35 | """The constructor of the class should set up needed 36 | resources, such as opening and parsing a configuration file. 37 | 38 | It is a good idea to keep whatever connection handles, data 39 | access objects, or other resources needed to retrieve the 40 | settings, as unprocessed as possible. The methods that 41 | actually need the data (:py:meth:`has`, :py:meth:`get`, 42 | :py:meth:`subsection`, :py:meth:`subsections` and possibly 43 | :py:meth:`typed`) should use those resources directly instead 44 | of reading from cached locally stored copies. 45 | 46 | The constructor must call the superclass' ``__init__`` method with all 47 | remaining keyword arguments, ie. ``super(MySource, 48 | self).__init__(**kwargs)``. 49 | 50 | """ 51 | 52 | self.identifier = kwargs.get('identifier', 53 | self.__class__.__name__.lower()) 54 | self.writable = kwargs.get('writable', False) 55 | self.parent = kwargs.get('parent') 56 | self.source = None 57 | 58 | @abstractmethod 59 | def has(self, key): 60 | """This method should return true if the parameter identified by 61 | ``key`` is present in this configuration source. It is up to 62 | each configuration source to define the semantics of what 63 | exactly "is present" means, but a guideline is that only real 64 | values should count as being present. If you only have some 65 | sort of placeholder or typing information for ``key`` this 66 | should probably not return True. 67 | 68 | Note that it is possible that a configuration source would 69 | return True for ``typed(some_key)`` and at the same time 70 | return False for ``has(some_key)``, if the source only carries 71 | typing information, not real values. 72 | 73 | """ 74 | pass # pragma: no cover 75 | 76 | @abstractmethod 77 | def get(self, key): 78 | """Should return the actual value of the parameter identified by 79 | ``key``. If ``has(some_key)`` returns True, ``get(some_key)`` 80 | should always succeed. If the configuration source does not 81 | include intrinsic typing information (ie. everything looks 82 | like a string) this method should return the string as-is, 83 | LayeredConfig is responsible for converting it to the correct 84 | type.""" 85 | pass # pragma: no cover 86 | 87 | @abstractmethod 88 | def keys(self): pass # pragma: no cover 89 | 90 | @abstractmethod 91 | def typed(self, key): 92 | """Should return True if this source contains typing information for 93 | ``key``, ie information about which data type this parameter 94 | should be. 95 | 96 | For sources where everything is stored as a string, this 97 | should generally return False (no way of distinguishing an 98 | actual string from a date formatted as a string). 99 | """ 100 | pass # pragma: no cover 101 | 102 | @abstractmethod 103 | def subsections(self): 104 | """Should return a list (or other iterator) of subsection keys, ie 105 | names that represent subsections of this configuration 106 | source. Not all configuration sources need to support 107 | subsections. In that case, this should just return an empty 108 | list. 109 | 110 | """ 111 | pass # pragma: no cover 112 | 113 | @abstractmethod 114 | def subsection(self, key): 115 | """Should return the subsection identified by ``key``, in the form of 116 | a new object of the same class, but initialized 117 | differently. Exactly how will depend on the source, but as a 118 | general rule the same resource handle used as ``self.source`` 119 | should be passed to the new object. Often, the subsection key 120 | will need to be provided to the new object as well, so that 121 | :py:meth:`get` and other methods can use it to look in the 122 | correct place. 123 | 124 | As a general rule, the constructor should be called with a 125 | ``parent`` parameter set to ``self``. 126 | """ 127 | pass # pragma: no cover 128 | 129 | @abstractmethod 130 | def set(self, key, value): 131 | """Should set the parameter identified by ``key`` to the new value 132 | ``value``. 133 | 134 | This method should be prepared for any type of value, ie ints, 135 | lists, dates, bools... If the backend cannot handle the given 136 | type, it should convert to a str itself. 137 | 138 | Note that this does not mean that the changes should be 139 | persisted in the backend data, only in the existing objects 140 | view of the data (only when :py:meth:`save` is called, the 141 | changes should be persisted). 142 | """ 143 | pass # pragma: no cover 144 | 145 | def setup(self, config): 146 | """Perform some post-initialization setup. This method will be called 147 | by the LayeredConfig constructor after its internal initialization is 148 | finished, with itself as argument. Sources may access all properties 149 | of the config object in order to eg. find out which parameters have 150 | been defined. 151 | 152 | The sources will be called in the same order as they were 153 | provided to the LayeredConfig constructior, ie. lowest 154 | precedence first. 155 | 156 | :param config: The initialized config object that this source is a part of 157 | :type config: layeredconfig.LayeredConfig 158 | """ 159 | pass 160 | 161 | def save(self): 162 | 163 | """Persist changed data to the backend. This generally means to update 164 | a loaded configuration file with all changed data, or similar. 165 | 166 | This method will only ever be called if :py:data:`writable` is 167 | True, and only if :py:data:`dirty` has been set to True. 168 | 169 | If your source is read-only, you don't have to implement this method. 170 | """ 171 | pass 172 | 173 | # @abstractmethod 174 | # should this be called "coerce", "cast" or something similar 175 | def typevalue(self, key, value): 176 | """Given a parameter identified by ``key`` and an untyped string, 177 | convert that string to the type that our version of key has. 178 | 179 | """ 180 | 181 | def listconvert(value): 182 | # this function might be called with both string 183 | # represenations of entire lists and simple (unquoted) 184 | # strings. String representations come in two flavours, 185 | # the (legacy/deprecated) python literal (eg "['foo', 186 | # 'bar']") and the simple (eg "foo, bar") The 187 | # ast.literal_eval handles the first case, and if the 188 | # value can't be parsed as a python expression, the second 189 | # way is attempted. If both fail, it is returned verbatim 190 | # (not wrapped in a list, for reasons) 191 | try: 192 | return ast.literal_eval(value) 193 | except (SyntaxError, ValueError): 194 | if "," in value: 195 | return [x.strip() for x in value.split(",")] 196 | else: 197 | return value 198 | 199 | # self.get(key) should never fail 200 | default = self.get(key) 201 | # if type(default) == type: 202 | if inspect.isclass(default): 203 | # print("Using class for %s" % key) 204 | t = default 205 | else: 206 | # print("Using instance for %s" % key) 207 | t = type(default) 208 | 209 | if t == bool: 210 | t = LayeredConfig.boolconvert 211 | elif t == list: 212 | t = listconvert 213 | elif t == date: 214 | t = LayeredConfig.dateconvert 215 | elif t == datetime: 216 | t = LayeredConfig.datetimeconvert 217 | # print("Converting %r to %r" % (value,t(value))) 218 | return t(value) 219 | 220 | # Internal function for now, until we find a generalized 221 | # extensible way of handling type conversions 222 | def _strvalue(self, value): 223 | if isinstance(value, list): # really any iterable but not 224 | # strings... if any of the elements contain " " or ", " 225 | # use literal syntax, otherwise use simple syntax 226 | if [x for x in value if " " in x or "," in x]: 227 | # can't use repr or str because unicode strings on py2 228 | # will result in a literal like "[u'foo', u'bar']", we 229 | # want "[u'foo', u'bar']" 230 | return "[%s]" % ", ".join(["'"+x.replace("'", "\'")+"'" for x in value]) 231 | else: 232 | return ", ".join(value) 233 | else: 234 | return str(value) 235 | -------------------------------------------------------------------------------- /layeredconfig/defaults.py: -------------------------------------------------------------------------------- 1 | from . import DictSource 2 | 3 | class Defaults(DictSource): 4 | def __init__(self, defaults=None, **kwargs): 5 | """ 6 | This source is initialized with a dict. 7 | 8 | :param defaults: A dict with configuration keys and values. If 9 | any values are dicts, these are turned into 10 | nested config objects. 11 | :type defaults: dict 12 | """ 13 | super(Defaults, self).__init__(**kwargs) 14 | if defaults: 15 | self.source = defaults 16 | # if not, DictSource.__init__ ensures that self.source is 17 | # a empty dict 18 | -------------------------------------------------------------------------------- /layeredconfig/dictsource.py: -------------------------------------------------------------------------------- 1 | # this should possibly be a abstract class as well 2 | from . import ConfigSource 3 | 4 | 5 | class DictSource(ConfigSource): 6 | def __init__(self, **kwargs): 7 | """If your backend data is exposable as a python dict, you can 8 | subclass from this class to avoid implementing :py:meth:`has`, 9 | :py:meth:`get`, :py:meth:`keys`, :py:meth:`subsection` and 10 | :py:meth:`subsections`. You only need to write 11 | :py:meth:`__init__` (which should set ``self.source`` to that 12 | exposed dict), and possibly :py:meth:`typed` and 13 | :py:meth:`save`. 14 | 15 | """ 16 | super(DictSource, self).__init__(**kwargs) 17 | self.source = {} 18 | 19 | def subsections(self): 20 | for (k, v) in self.source.items(): 21 | if isinstance(v, dict): 22 | yield k 23 | 24 | def keys(self): 25 | for (k, v) in self.source.items(): 26 | if not isinstance(v, dict) and not isinstance(v, type): 27 | yield k 28 | 29 | def subsection(self, key): 30 | # Make an object of the correct type 31 | return self.__class__(defaults=self.source[key], 32 | parent=self, 33 | identifier=self.identifier) 34 | 35 | def typed(self, key): 36 | # if we have it, we can type it 37 | return key in self.source and self.source[key] is not None 38 | 39 | def has(self, key): 40 | # should return true for real values only, not type placeholders or sub-dicts 41 | return key in self.source and not isinstance(self.source[key], (type, dict)) 42 | 43 | def get(self, key): 44 | return self.source[key] 45 | 46 | def set(self, key, value): 47 | self.source[key] = value 48 | -------------------------------------------------------------------------------- /layeredconfig/environment.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from . import ConfigSource 4 | 5 | class Environment(ConfigSource): 6 | def __init__(self, 7 | environ=None, 8 | prefix=None, 9 | lower=True, 10 | sectionsep="_", 11 | **kwargs): 12 | """Loads settings from environment variables. If ``prefix`` is set to 13 | ``MYAPP_``, the value of the environment variable ``MYAPP_HOME`` 14 | will be available as the configuration setting ``home``. 15 | 16 | :param environ: Environment variables, in dict form like 17 | :py:data:`os.environ`. If not provided, uses 18 | the real :py:data:`os.environ`. 19 | :type environ: dict 20 | :param prefix: Since the entire environment is not suitable to use 21 | as a configuration, only variables starting with this 22 | prefix are used. 23 | :type prefix: str 24 | :param lower: If true, lowercase the name of environment 25 | variables (since these typically uses uppercase) 26 | :type lower: True 27 | :param sectionsep: An alternate section separator instead of ``-``. 28 | :type sectionsep: str 29 | """ 30 | super(Environment, self).__init__(**kwargs) 31 | if environ is None: 32 | if kwargs.get("empty"): 33 | environ = {} 34 | else: 35 | environ = os.environ 36 | if prefix is None: 37 | prefix = "" 38 | 39 | self.source = environ 40 | self.prefix = prefix 41 | self.sectionsep = sectionsep 42 | 43 | # used by both keys and subsections, but in different ways 44 | def _internalkeys(self): 45 | return [x.lower()[len(self.prefix):] for x in self.source.keys() if x.startswith(self.prefix)] 46 | 47 | def keys(self): 48 | for x in self._internalkeys(): 49 | if self.sectionsep not in x: 50 | yield x 51 | 52 | def has(self, key): 53 | # reverse the prefix/lowerize stuff 54 | k = self.prefix + key.upper() 55 | return k in self.source 56 | 57 | def get(self, key): 58 | k = self.prefix + key.upper() 59 | return self.source[k] 60 | 61 | def set(self, key, val): 62 | k = self.prefix + key.upper() 63 | self.source[k] = val 64 | 65 | def typed(self, key): 66 | return False 67 | 68 | def subsections(self): 69 | yielded = set() 70 | for x in self._internalkeys(): 71 | if self.sectionsep in x: 72 | section = x.split(self.sectionsep)[0] 73 | if section not in yielded: 74 | yield(section) 75 | yielded.add(section) 76 | 77 | def subsection(self, key): 78 | s = key.upper() + self.sectionsep 79 | newenviron = dict([(k.replace(s,"", 1), v) for k, v in self.source.items() if s in k]) 80 | return Environment(newenviron, 81 | prefix=self.prefix, 82 | sectionsep=self.sectionsep, 83 | parent=self, 84 | identifier=self.identifier) 85 | -------------------------------------------------------------------------------- /layeredconfig/etcdstore.py: -------------------------------------------------------------------------------- 1 | import six 2 | from . import ConfigSource 3 | 4 | import requests 5 | 6 | class EtcdStore(ConfigSource): 7 | 8 | def __init__(self, baseurl="http://127.0.0.1:2379/v2/", 9 | **kwargs): 10 | """Loads configuration from a `etcd store 11 | `_. 12 | 13 | :param baseurl: The main endpoint of the etcd store 14 | 15 | ``etcd`` has no concept of typed values, so all data from this 16 | source are returned as strings. 17 | """ 18 | super(EtcdStore, self).__init__(**kwargs) 19 | if kwargs.get('source'): 20 | # subsection 21 | self.source = kwargs['source'] 22 | self.sectionkey = kwargs['sectionkey'] 23 | else: 24 | self.source = baseurl + "keys" 25 | self.sectionkey = "/" 26 | resp = requests.get(self.source + self.sectionkey) 27 | self.values = resp.json()['node']['nodes'] 28 | self.dirtyvalues = {} 29 | self.writable = kwargs.get("writable", True) 30 | self.subsectioncache = {} 31 | 32 | def has(self, key): 33 | for child in self.values: 34 | if 'dir' not in child and self.sectionkey + key == child['key']: 35 | return True 36 | return False 37 | 38 | def get(self, key): 39 | for child in self.values: 40 | if self.sectionkey + key == child['key']: 41 | return child['value'] 42 | raise KeyError(key) 43 | 44 | def keys(self): 45 | for child in self.values: 46 | if 'dir' not in child: 47 | yield child['key'][len(self.sectionkey):] 48 | 49 | def typed(self, key): 50 | return False # in etcd, all keys seem to be strings. Or can 51 | # they be ints, bools and lists (JSON supported 52 | # types) maybe? 53 | 54 | def subsections(self): 55 | for child in self.values: 56 | if 'dir' in child: 57 | yield child['key'][len(self.sectionkey):] 58 | 59 | def subsection(self, key): 60 | if key not in self.subsectioncache: 61 | prefix = self.sectionkey 62 | if not prefix.endswith("/"): 63 | prefix += "/" 64 | self.subsectioncache[key] = EtcdStore(source=self.source, 65 | parent=self, 66 | sectionkey=prefix+key+"/") 67 | return self.subsectioncache[key] 68 | 69 | def set(self, key=None, value=None): 70 | self.dirty = True 71 | if self.parent: 72 | self.parent.set() 73 | if key and value: 74 | self.dirtyvalues[key] = value 75 | 76 | def _strvalue(self, value): 77 | if isinstance(value, bool): 78 | return str(value).lower() 79 | else: 80 | return super(EtcdStore, self)._strvalue(value) 81 | 82 | def save(self): 83 | for k in self.dirtyvalues: 84 | requests.put(self.source+self.sectionkey+k, 85 | data={'value': self._strvalue(self.dirtyvalues[k])}) 86 | self.dirtyvalues = {} 87 | for subsection in self.subsections(): 88 | self.subsection(subsection).save() 89 | self.dirty = False 90 | -------------------------------------------------------------------------------- /layeredconfig/inifile.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import logging 3 | import os 4 | import sys 5 | import six 6 | from six.moves import configparser 7 | from six import text_type as str 8 | try: 9 | from collections import OrderedDict 10 | except ImportError: # pragma: no cover 11 | # if on python 2.6 12 | from ordereddict import OrderedDict 13 | 14 | from . import ConfigSource 15 | 16 | 17 | class INIFile(ConfigSource): 18 | def __init__(self, 19 | inifilename=None, 20 | rootsection="__root__", 21 | sectionsep=".", 22 | writable=True, 23 | **kwargs): 24 | """Loads and optionally saves configuration files in INI format, as 25 | handled by :py:mod:`configparser`. 26 | 27 | :param inifile: The name of a ini-style configuration 28 | file. The file should have a top-level 29 | section, by default named ``__root__``, whose 30 | keys are turned into top-level configuration 31 | parameters. Any other sections in this file 32 | are turned into nested config objects. 33 | :type inifile: str 34 | :param rootsection: An alternative name for the top-level section. 35 | See note below. 36 | :type rootsection: str 37 | :param sectionsep: separator to use in section names to 38 | separate nested subsections. See note below. 39 | :type sectionsep: str 40 | :param writable: Whether changes to the LayeredConfig object 41 | that has this INIFile object amongst its 42 | sources should be saved in the INI file. 43 | :type writable: bool 44 | 45 | .. note:: 46 | 47 | Nested subsections is possible, but since the INI format 48 | does not natively support nesting, this is accomplished 49 | through specially-formatted section names, eg the config 50 | value mymodule.mysection.example would be expressed in the 51 | ini file as:: 52 | 53 | [mymodule.mysection] 54 | example = value 55 | 56 | Since this source uses :py:mod:`configparser`, and since 57 | that module handles sections named ``[DEFAULT]`` 58 | differently, this module will have a sort-of automatic 59 | cascading feature for subsections if ``DEFAULT`` is used as 60 | ``rootsection`` 61 | 62 | """ 63 | super(INIFile, self).__init__(**kwargs) 64 | if inifilename: 65 | if not os.path.exists(inifilename): 66 | logging.warning("INI file %s does not exist" % inifilename) 67 | # create a empty RawConfigParser (Raw to avoid the 68 | # interpolation behaviour of other classes) 69 | self.source = configparser.RawConfigParser(dict_type=OrderedDict) 70 | self.inifilename = inifilename 71 | if rootsection != "DEFAULT": 72 | self.source.add_section(rootsection) 73 | else: 74 | self.source = configparser.RawConfigParser(dict_type=OrderedDict) 75 | if sys.version_info >= (3,2): 76 | reader = self.source.read_file 77 | else: 78 | reader = self.source.readfp 79 | # we don't know the encoding of this file; assume utf-8 80 | with codecs.open(inifilename, encoding="utf-8") as fp: 81 | reader(fp) 82 | 83 | self.inifilename = inifilename 84 | # only used when creating new INIFile objects internally 85 | elif 'config' in kwargs: 86 | self.source = kwargs['config'] 87 | self.inifilename = None 88 | else: 89 | # This is an "empty" INIFile object 90 | self.source = configparser.ConfigParser(dict_type=OrderedDict) 91 | self.source.add_section(rootsection) 92 | self.inifilename = None 93 | if 'section' in kwargs: 94 | self.sectionkey = kwargs['section'] 95 | else: 96 | self.sectionkey = rootsection 97 | self.dirty = False 98 | self.writable = writable 99 | self.rootsection = rootsection 100 | self.sectionsep = sectionsep 101 | 102 | def typed(self, key): 103 | # INI files carry no intrinsic type information 104 | return False 105 | 106 | def subsections(self): 107 | # self.source may be None if we provided the path to a 108 | # nonexistent inifile (this should probably throw an exception 109 | # instead) 110 | if not self.source: 111 | return [] 112 | else: 113 | allsections = [x for x in self.source.sections() if x != self.rootsection] 114 | if self.sectionkey != self.rootsection: 115 | # find out what subsections are under this subsection (eg nested sections) 116 | return [x[len(self.sectionkey+self.sectionsep):].split(self.sectionsep)[0] for x in allsections if x.startswith(self.sectionkey+self.sectionsep)] 117 | else: 118 | return [x for x in allsections if self.sectionsep not in x] 119 | 120 | def subsection(self, key): 121 | if self.sectionkey == self.rootsection: 122 | section = key 123 | else: 124 | section = self.sectionkey + self.sectionsep + key 125 | return INIFile(config=self.source, section=section, 126 | parent=self, identifier=self.identifier) 127 | 128 | def has(self, key): 129 | if self.sectionkey == "DEFAULT": 130 | return key in self.source.defaults() 131 | else: 132 | return self.source.has_option(self.sectionkey, key) 133 | 134 | def get(self, key): 135 | return str(self.source.get(self.sectionkey, key)) 136 | 137 | def set(self, key, value): 138 | self.source.set(self.sectionkey, key, self._strvalue(value)) 139 | 140 | def keys(self): 141 | if self.source.has_section(self.sectionkey): 142 | for k in self.source.options(self.sectionkey): 143 | yield k 144 | 145 | def save(self): 146 | # this should only be called on root objects 147 | assert not self.parent, "save() should only be called on root objects" 148 | if self.inifilename: 149 | with open(self.inifilename, "w") as fp: 150 | self.source.write(fp) 151 | -------------------------------------------------------------------------------- /layeredconfig/jsonfile.py: -------------------------------------------------------------------------------- 1 | import json 2 | from six import text_type as str 3 | 4 | from . import DictSource 5 | 6 | 7 | class JSONFile(DictSource): 8 | 9 | def __init__(self, jsonfilename=None, writable=True, **kwargs): 10 | """Loads and optionally saves configuration files in JSON 11 | format. Since JSON has some support for typed values (supports 12 | numbers, lists, bools, but not dates or datetimes), data from 13 | this source are sometimes typed, sometimes only available as 14 | strings. 15 | 16 | :param jsonfile: The name of a JSON file, whose root element 17 | should be a JSON object (python dict). Nested 18 | objects are turned into nested config objects. 19 | :type jsonfile: str 20 | :param writable: Whether changes to the LayeredConfig object 21 | that has this JSONFile object amongst its 22 | sources should be saved in the JSON file. 23 | :type writable: bool 24 | 25 | """ 26 | super(JSONFile, self).__init__(**kwargs) 27 | if jsonfilename == None and 'parent' in kwargs and hasattr(kwargs['parent'], 'jsonfilename'): 28 | jsonfilename = kwargs['parent'].jsonfilename 29 | if 'defaults' in kwargs: 30 | self.source = kwargs['defaults'] 31 | elif kwargs.get('empty', False): 32 | self.source = {} 33 | else: 34 | with open(jsonfilename) as fp: 35 | self.source = json.load(fp) 36 | self.jsonfilename = jsonfilename 37 | self.dirty = False 38 | self.writable = writable 39 | 40 | def typed(self, key): 41 | # if the value is anything other than a string, we can be sure 42 | # that it contains useful type information. 43 | 44 | return self.has(key) and not isinstance(self.get(key), str) 45 | 46 | def set(self, key, value): 47 | # simple stringification -- should perhaps only be done in the 48 | # save step through a method passed as a default parameter to 49 | # json dumps 50 | self.source[key] = str(value) 51 | 52 | def save(self): 53 | assert not self.parent, "save() should only be called on root objects" 54 | if self.jsonfilename: 55 | with open(self.jsonfilename, "w") as fp: 56 | json.dump(self.source, fp, indent=4, separators=(',',': '), sort_keys=True) 57 | -------------------------------------------------------------------------------- /layeredconfig/layeredconfig.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import itertools 4 | import logging 5 | from datetime import datetime, date 6 | 7 | try: 8 | from collections import OrderedDict 9 | except ImportError: # pragma: no cover 10 | # if on python 2.6 11 | from ordereddict import OrderedDict 12 | 13 | 14 | class LayeredConfig(object): 15 | def __init__(self, *sources, **kwargs): 16 | """Creates a config object from one or more sources and provides 17 | unified access to a nested set of configuration 18 | parameters. The source of these parameters a config file 19 | (using .ini-file style syntax), command line parameters, and 20 | default settings embedded in code. Command line parameters 21 | override configuration file parameters, which in turn override 22 | default settings in code (hence **Layered** Config). 23 | 24 | Configuration parameters are accessed as regular object 25 | attributes, not dict-style key/value pairs. Configuration 26 | parameter names should therefore be regular python 27 | identifiers, and preferrably avoid upper-case and "_" as well 28 | (i.e. only consist of the characters a-z and 0-9) 29 | 30 | Configuration parameter values can be typed (strings, 31 | integers, booleans, dates, lists...). Even though some sources 32 | lack typing information (eg in INI files, command-line 33 | parameters and enviroment variables, everything is a string), 34 | LayeredConfig will attempt to find typing information in other 35 | sources and convert data. 36 | 37 | :param \*sources: Initialized ConfigSource-derived objects 38 | :param cascade: If an attempt to get a non-existing parameter 39 | on a sub (nested) configuration object should 40 | attempt to get the parameter on the parent 41 | config object. ``False`` by default, 42 | :type cascade: bool 43 | :param writable: Whether configuration values should be mutable. 44 | ``True`` by default. This does not affect 45 | :py:meth:`~Layeredconfig.set`. 46 | :type writable: bool 47 | 48 | """ 49 | self._sources = sources 50 | self._subsections = OrderedDict() 51 | self._cascade = kwargs.get('cascade', False) 52 | self._writable = kwargs.get('writable', True) 53 | self._parent = None 54 | self._sectionkey = None 55 | 56 | # Each source may have any number of named subsections. We 57 | # create a LayeredConfig object for each name, and stuff all 58 | # matching subections from each of our sources in it. 59 | # 60 | # 1. find all names 61 | sectionkeys = [] 62 | for src in self._sources: 63 | try: 64 | for k in src.subsections(): 65 | if k not in sectionkeys: 66 | sectionkeys.append(k) 67 | except AttributeError: # possibly others, or all 68 | # we couldn't get any subsections for source, perhaps 69 | # because it's an "empty" source. Well, that's ok. 70 | pass 71 | 72 | for k in sectionkeys: 73 | # 2. find all subsections in all of our sources 74 | s = [] 75 | for src in self._sources: 76 | if k in list(src.subsections()): 77 | s.append(src.subsection(k)) 78 | else: 79 | # create an "empty" subsection object. It's 80 | # important that all the LayeredConfig objects in a 81 | # tree have the exact same set of 82 | # ConfigSource-derived types. 83 | # print("creating empty %s" % src.__class__.__name__) 84 | s.append(src.__class__(parent=src, 85 | identifier=src.identifier, 86 | writable=src.writable, 87 | empty=True, 88 | cascade=self._cascade)) 89 | # 3. create a LayeredConfig object for the subsection 90 | c = self.__class__(*s, 91 | cascade=self._cascade, 92 | writable=self._writable) 93 | c._sectionkey = k 94 | c._parent = self 95 | self._subsections[k] = c 96 | 97 | # 4. give each source a chance to to some post-init setup. 98 | for src in self._sources: 99 | src.setup(self) 100 | 101 | @staticmethod 102 | def write(config): 103 | """Commits any pending modifications, ie save a configuration file if 104 | it has been marked "dirty" as a result of an normal 105 | assignment. The modifications are written to the first 106 | writable source in this config object. 107 | 108 | .. note:: 109 | 110 | This is a static method, ie not a method on any object 111 | instance. This is because all attribute access on a 112 | LayeredConfig object is meant to retrieve configuration 113 | settings. 114 | 115 | :param config: The configuration object to save 116 | :type config: layeredconfig.LayeredConfig 117 | 118 | """ 119 | root = config 120 | while root._parent: 121 | root = root._parent 122 | 123 | for source in root._sources: 124 | if source.writable and source.dirty: 125 | source.save() 126 | 127 | @staticmethod 128 | def set(config, key, value, sourceid="defaults"): 129 | """Sets a value in this config object *without* marking any source 130 | dirty, and with exact control of exactly where to set the 131 | value. This is mostly useful for low-level trickery with 132 | config objects. 133 | 134 | :param config: The configuration object to set values on 135 | :param key: The parameter name 136 | :param value: The new value 137 | :param sourceid: The identifier for the underlying source that the 138 | value should be set on. 139 | """ 140 | for source in config._sources: 141 | if source.identifier == sourceid: 142 | source.set(key, value) 143 | # What if no source is found? We silently ignore... 144 | 145 | @staticmethod 146 | def get(config, key, default=None): 147 | """Gets a value from the config object, or return a default value if 148 | the parameter does not exist, like :py:meth:`dict.get` does. 149 | """ 150 | 151 | if hasattr(config, key): 152 | return getattr(config, key) 153 | else: 154 | return default 155 | 156 | @staticmethod 157 | def dump(config): 158 | """Returns the entire content of the config object in a way that can 159 | be easily examined, compared or dumped to a string or file. 160 | 161 | :param config: The configuration object to dump 162 | :rtype: dict 163 | 164 | """ 165 | def _dump(element): 166 | if not isinstance(element, config.__class__): 167 | return element 168 | 169 | section = dict() 170 | for key, subsection in element._subsections.items(): 171 | section[key] = _dump(subsection) 172 | for key in element: 173 | section[key] = getattr(element, key) 174 | return section 175 | 176 | return _dump(config) 177 | 178 | # These are methods i'd like to implement next 179 | # 180 | # @staticmethod 181 | # def where(config, key): 182 | # """returns the identifier of a source where a given key is found, or None.""" 183 | # pass 184 | # 185 | # @staticmethod 186 | # def load(config, d): 187 | # """Recreates a dump()ed config object.""" 188 | # pass 189 | 190 | @staticmethod 191 | def datetimeconvert(value): 192 | """Convert the string *value* to a :py:class:`~datetime.datetime` 193 | object. *value* is assumed to be on the form "YYYY-MM-DD 194 | HH:MM:SS" (optionally ending with fractions of a second). 195 | 196 | """ 197 | try: 198 | return datetime.strptime(value, "%Y-%m-%d %H:%M:%S.%f") 199 | except ValueError: 200 | return datetime.strptime(value, "%Y-%m-%d %H:%M:%S") 201 | 202 | @staticmethod 203 | def dateconvert(value): 204 | """Convert the string *value* to a :py:class:`~datetime.date` 205 | object. *value* is assumed to be on the form "YYYY-MM-DD". 206 | 207 | """ 208 | return datetime.strptime(value, "%Y-%m-%d").date() 209 | 210 | @staticmethod 211 | def boolconvert(value): 212 | """Convert the string *value* to a boolean. ``"True"`` is converted to 213 | ``True`` and ``"False"`` is converted to ``False``. 214 | 215 | .. note:: 216 | 217 | If value is neither "True" nor "False", it's returned unchanged. 218 | 219 | """ 220 | # not all bools should be converted, see test_typed_commandline 221 | if value == "True": 222 | return True 223 | elif value == "False": 224 | return False 225 | else: 226 | return value 227 | 228 | def __repr__(self): 229 | return self.dump(self).__repr__() 230 | 231 | def __iter__(self): 232 | l = set() 233 | 234 | iterables = [x.keys() for x in self._sources] 235 | 236 | if self._cascade: 237 | c = self 238 | while c._parent: 239 | iterables.append(c._parent) 240 | c = c._parent 241 | 242 | for k in itertools.chain(*iterables): 243 | if k not in l: 244 | l.add(k) 245 | yield k 246 | 247 | def __getattr__(self, name): 248 | 249 | if name in self._subsections: 250 | return self._subsections[name] 251 | 252 | found = False 253 | # find the appropriate value in the highest-priority source 254 | for source in reversed(self._sources): 255 | # if self._cascade, we must climb the entire chain of 256 | # .parent objects to be sure. 257 | done = False 258 | while not done: 259 | if source.has(name): 260 | found = True 261 | done = True # we found it 262 | elif self._cascade and source.parent: 263 | source = source.parent 264 | else: 265 | done = True # we didn't find it 266 | if found: 267 | break 268 | 269 | if found: 270 | if source.typed(name): 271 | return source.get(name) 272 | else: 273 | # we need to find a typesource for this value. 274 | done = False 275 | this = self 276 | while not done: 277 | for typesource in reversed(this._sources): 278 | if typesource.typed(name): 279 | done = True 280 | break 281 | if not done and self._cascade and this._parent: 282 | # Iterate up the parent chain to find it. 283 | this = this._parent 284 | else: 285 | done = True 286 | 287 | if typesource.typed(name): 288 | return typesource.typevalue(name, source.get(name)) 289 | else: 290 | # we can't type this data, return as-is 291 | return source.get(name) 292 | else: 293 | if self._cascade and self._parent and name not in self._parent._subsections: 294 | return self._parent.__getattr__(name) 295 | 296 | raise AttributeError("Configuration key %s doesn't exist" % name) 297 | 298 | def __setattr__(self, name, value): 299 | # print("__setattribute__ %s to %s" % (name,value)) 300 | if name.startswith("_"): 301 | object.__setattr__(self, name, value) 302 | return 303 | 304 | # we need to get access to two sources: 305 | 306 | # 1. the highest-priority writable source (regardless of 307 | # whether it originally had this value) 308 | found = False 309 | for writesource in reversed(self._sources): 310 | if writesource.writable: 311 | found = True 312 | break 313 | if found: 314 | writesource.set(name, value) 315 | writesource.dirty = True 316 | while writesource.parent: 317 | writesource = writesource.parent 318 | writesource.dirty = True 319 | 320 | # 2. the highest-priority source that has this value (typed or 321 | # not) or contains typing info for it. 322 | found = False 323 | for source in reversed(self._sources): 324 | if source.has(name) or source.typed(name): 325 | found = True 326 | break 327 | if found: 328 | source.set(name, value) # regardless of typing 329 | elif self._cascade and self._parent: 330 | return self._parent.__setattr__(name, value) 331 | else: 332 | raise AttributeError("Configuration key %s doesn't exist" % name) 333 | -------------------------------------------------------------------------------- /layeredconfig/plistfile.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import codecs 3 | import plistlib 4 | import sys 5 | 6 | from six import text_type as str 7 | from six import binary_type as bytes 8 | 9 | from . import DictSource 10 | 11 | 12 | class PListFile(DictSource): 13 | def __init__(self, plistfilename=None, writable=True, **kwargs): 14 | """Loads and optionally saves configuration files in PList 15 | format. Since PList has some support for typed values (supports 16 | numbers, lists, bools, datetimes *but not dates*), data from 17 | this source are sometimes typed, sometimes only available as 18 | strings. 19 | 20 | :param plistfile: The name of a PList file. Nested sections are 21 | turned into nested config objects. 22 | :type plistfile: str 23 | :param writable: Whether changes to the LayeredConfig object 24 | that has this PListFile object amongst its 25 | sources should be saved in the PList file. 26 | :type writable: bool 27 | """ 28 | if sys.version_info >= (3,4): 29 | self.reader = plistlib.load 30 | self.writer = plistlib.dump 31 | else: 32 | self.reader = plistlib.readPlist 33 | self.writer = plistlib.writePlist 34 | super(PListFile, self).__init__(**kwargs) 35 | if plistfilename == None and 'parent' in kwargs and hasattr(kwargs['parent'], 'plistfilename'): 36 | plistfilename = kwargs['parent'].plistfilename 37 | if 'defaults' in kwargs: 38 | self.source = kwargs['defaults'] 39 | elif kwargs.get('empty', False): 40 | self.source = {} 41 | else: 42 | with open(plistfilename, "rb") as fp: 43 | self.source = self.reader(fp) 44 | self.plistfilename = plistfilename 45 | self.dirty = False 46 | self.encoding = "utf-8" # I hope this is a sensible default 47 | self.writable = writable 48 | 49 | def set(self, key, value): 50 | # plist natively supports some types but not all (notably not date) 51 | if not isinstance(value, (str, bool, int, list, datetime)): 52 | value = str(value) 53 | super(PListFile, self).set(key, value) 54 | 55 | def get(self, key): 56 | ret = super(PListFile, self).get(key) 57 | if isinstance(ret, bytes): 58 | ret = ret.decode(self.encoding) 59 | # same with individual elements of lists 60 | elif isinstance(ret, list): 61 | for idx, val in enumerate(ret): 62 | if isinstance(ret[idx], bytes): 63 | ret[idx] = ret[idx].decode(self.encoding) 64 | return ret 65 | 66 | def typed(self, key): 67 | # if the value is anything other than a string, we can be sure 68 | # that it contains useful type information. 69 | return self.has(key) and not isinstance(self.get(key), str) 70 | 71 | def keys(self): 72 | for k in super(PListFile, self).keys(): 73 | if isinstance(k, bytes): 74 | k = k.decode(self.encoding) 75 | yield k 76 | 77 | def subsections(self): 78 | for k in super(PListFile, self).subsections(): 79 | if isinstance(k, bytes): 80 | k = k.decode(self.encoding) 81 | yield k 82 | 83 | def save(self): 84 | assert not self.parent, "save() should only be called on root objects" 85 | if self.plistfilename: 86 | with open(self.plistfilename, "wb") as fp: 87 | self.writer(self.source, fp) 88 | -------------------------------------------------------------------------------- /layeredconfig/pyfile.py: -------------------------------------------------------------------------------- 1 | import six 2 | from . import ConfigSource 3 | 4 | import inspect 5 | 6 | class PyFile(ConfigSource): 7 | 8 | def __init__(self, pyfilename=None, **kwargs): 9 | """Loads configuration from a python source file. Any variables 10 | defined in that file will be interpreted as configuration 11 | keys. The class ``Subsection`` is automatically imported into 12 | the context when the file is executed, and represents a 13 | subsection of the configuration. Any attribute set on such an 14 | object is treated as a configuration parameter on that 15 | subsection. 16 | 17 | .. note:: 18 | 19 | The python source file is loaded and interpreted once, when 20 | creating the PyFile object. If a value is set by 21 | eg. calling a function, that function will only be called 22 | at load time, not when accessing the parameter. 23 | 24 | :param pyfile: The name of a file containing valid python code. 25 | :type pyfile: str 26 | 27 | """ 28 | super(PyFile, self).__init__(**kwargs) 29 | self.source = Subsection() 30 | if pyfilename: 31 | with open(pyfilename) as fp: 32 | pycode = compile(fp.read(), pyfilename, 'exec') 33 | six.exec_(pycode, globals(), self.source) 34 | elif kwargs.get('dict'): 35 | self.source = kwargs['dict'] 36 | 37 | def has(self, key): 38 | return key in self.source and not isinstance(key, Subsection) 39 | 40 | def get(self, key): 41 | return self.source.get(key) 42 | 43 | def keys(self): 44 | for key, val in self.source.items(): 45 | if not (isinstance(val, Subsection) or 46 | inspect.ismodule(val) or 47 | inspect.isfunction(val) or 48 | val.__class__.__module__ in ('__future__')): 49 | yield key 50 | 51 | def subsections(self): 52 | return [key for key in self.source.keys() if isinstance(self.source[key], Subsection)] 53 | 54 | def subsection(self, key): 55 | # wrap this somehow? 56 | return PyFile(pyfilename=None, dict=self.source.get(key)) 57 | 58 | def set(self, key, value): 59 | # as self.writable is False, this will never be called 60 | return ValueError("Cannot set variables in a pyfile") 61 | 62 | def typed(self, key): 63 | # if we have it, it's typed 64 | return self.has(key) 65 | 66 | class Subsection(dict): 67 | # pass 68 | 69 | # to allow subsect.key = value syntax, but still store data in 70 | # self, not self.__dict__ 71 | def __setattr__(self, key, value): 72 | self[key] = value 73 | 74 | def __getattr__(self, key): 75 | return self[key] 76 | -------------------------------------------------------------------------------- /layeredconfig/yamlfile.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | 3 | import yaml 4 | 5 | from . import DictSource 6 | 7 | class YAMLFile(DictSource): 8 | def __init__(self, yamlfilename=None, writable=True, **kwargs): 9 | """Loads and optionally saves configuration files in YAML 10 | format. Since YAML (and the library implementing the support, 11 | PyYAML) has automatic support for typed values, data from this 12 | source are typed. 13 | 14 | :param yamlfile: The name of a YAML file. Nested 15 | sections are turned into nested config objects. 16 | :type yamlfile: str 17 | :param writable: Whether changes to the LayeredConfig object 18 | that has this YAMLFile object amongst its 19 | sources should be saved in the YAML file. 20 | :type writable: bool 21 | 22 | """ 23 | 24 | 25 | super(YAMLFile, self).__init__(**kwargs) 26 | if yamlfilename == None and 'parent' in kwargs and hasattr(kwargs['parent'], 'yamlfilename'): 27 | yamlfilename = kwargs['parent'].yamlfilename 28 | if 'defaults' in kwargs: 29 | self.source = kwargs['defaults'] 30 | elif kwargs.get('empty', False): 31 | self.source = {} 32 | else: 33 | with codecs.open(yamlfilename, encoding="utf-8") as fp: 34 | # do we need safe_load? 35 | self.source = yaml.safe_load(fp.read()) 36 | self.yamlfilename = yamlfilename 37 | self.dirty = False 38 | self.writable = writable 39 | self.encoding = "utf-8" # not sure this is ever really needed 40 | 41 | def get(self, key): 42 | ret = super(YAMLFile, self).get(key) 43 | # pyyaml by default makes strings whose content fit in ascii 44 | # available (on python2) as str objects, not unicode. Undo 45 | # this sillyness. 46 | if isinstance(ret, bytes): 47 | ret = ret.decode(self.encoding) 48 | # same with individual elements of lists 49 | elif isinstance(ret, list): 50 | for idx, val in enumerate(ret): 51 | if isinstance(ret[idx], bytes): 52 | ret[idx] = ret[idx].decode(self.encoding) 53 | return ret 54 | 55 | def save(self): 56 | assert not self.parent, "save() should only be called on root objects" 57 | if self.yamlfilename: 58 | with codecs.open(self.yamlfilename, "w", encoding=self.encoding) as fp: 59 | yaml.safe_dump(self.source, fp, default_flow_style=False) 60 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | PyYAML 3 | unittest2 4 | ordereddict 5 | wheel 6 | six 7 | twine 8 | future 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | 6 | try: 7 | from setuptools import setup 8 | except ImportError: 9 | from distutils.core import setup 10 | 11 | badges = open('BADGES.rst').read() 12 | readme = open('README.rst').read() 13 | history = open('HISTORY.rst').read().replace('.. :changelog:', '') 14 | 15 | requirements = [ 16 | 'six', 17 | 'PyYAML', 18 | 'requests' 19 | ] 20 | 21 | if sys.version_info < (2, 7, 0): 22 | requirements.append('ordereddict >= 1.1') 23 | 24 | test_requirements = [ 25 | # TODO: put package test requirements here 26 | ] 27 | 28 | 29 | # we can't just "import layeredconfig" to get at 30 | # layeredconfig.__version__ since it might have unmet dependencies at 31 | # this point. Exctract it directly from the file (code from rdflib:s 32 | # setup.py) 33 | def find_version(filename): 34 | import re 35 | _version_re = re.compile(r'__version__ = "(.*)"') 36 | for line in open(filename): 37 | version_match = _version_re.match(line) 38 | if version_match: 39 | return version_match.group(1) 40 | 41 | setup( 42 | name='layeredconfig', 43 | version=find_version('layeredconfig/__init__.py'), 44 | description='Manages configuration coming from config files, environment variables, command line arguments, code defaults or other sources', 45 | long_description=badges + "\n\n" + readme + '\n\n' + history, 46 | author='Staffan Malmgren', 47 | author_email='staffan.malmgren@gmail.com', 48 | url='https://github.com/staffanm/layeredconfig', 49 | packages=[ 50 | 'layeredconfig', 51 | ], 52 | package_dir={'layeredconfig': 53 | 'layeredconfig'}, 54 | include_package_data=True, 55 | install_requires=requirements, 56 | license="BSD", 57 | zip_safe=False, 58 | keywords='configuration', 59 | classifiers=[ 60 | 'Development Status :: 3 - Alpha', 61 | 'Intended Audience :: Developers', 62 | 'License :: OSI Approved :: BSD License', 63 | 'Natural Language :: English', 64 | "Programming Language :: Python :: 2", 65 | 'Programming Language :: Python :: 2.6', 66 | 'Programming Language :: Python :: 2.7', 67 | 'Programming Language :: Python :: 3', 68 | 'Programming Language :: Python :: 3.3', 69 | 'Programming Language :: Python :: 3.4', 70 | 'Programming Language :: Python :: 3.5', 71 | ], 72 | test_suite='tests', 73 | tests_require=test_requirements 74 | ) 75 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals 4 | """ 5 | test_layeredconfig 6 | ---------------------------------- 7 | 8 | Tests for `layeredconfig` module. 9 | """ 10 | 11 | import os 12 | import sys 13 | if sys.version_info < (2, 7, 0): # pragma: no cover 14 | import unittest2 as unittest 15 | else: 16 | import unittest 17 | import shutil 18 | import six 19 | 20 | 21 | class Examples(unittest.TestCase): 22 | 23 | def _test_pyfile(self, pyfile, want=True, comparator=None, expectexit=False): 24 | # temporarily redefine the print function in current context 25 | l = locals() 26 | l['print'] = lambda x: None 27 | with open(pyfile) as fp: 28 | pycode = compile(fp.read(), pyfile, 'exec') 29 | try: 30 | result = six.exec_(pycode, globals(), l) 31 | # the exec:ed code is expected to set return_value 32 | got = locals()['return_value'] 33 | if not comparator: 34 | comparator = self.assertEqual 35 | comparator(want, got) 36 | except SystemExit as e: 37 | if not expectexit: 38 | raise e 39 | 40 | def test_firststep(self): 41 | self._test_pyfile("docs/examples/firststep.py") 42 | os.unlink("myapp.ini") 43 | 44 | def test_usage(self): 45 | shutil.copy2("docs/examples/myapp.ini", os.getcwd()) 46 | self._test_pyfile("docs/examples/usage.py") 47 | os.unlink("myapp.ini") 48 | 49 | def test_argparse(self): 50 | _stdout = sys.stdout 51 | try: 52 | devnull = open(os.devnull, "w") 53 | sys.stdout = devnull 54 | self._test_pyfile("docs/examples/argparse-example.py", expectexit=True) 55 | finally: 56 | sys.stdout = _stdout 57 | devnull.close() 58 | os.unlink("myapp.ini") 59 | 60 | def test_pyfile(self): 61 | shutil.copy2("docs/examples/pyfile-example.py", os.getcwd()) 62 | shutil.copy2("docs/examples/conf.py", os.getcwd()) 63 | shutil.copy2("docs/examples/pyfile-example2.py", os.getcwd()) 64 | shutil.copy2("docs/examples/defaults.py", os.getcwd()) 65 | self._test_pyfile("docs/examples/pyfile-example.py", "My App") 66 | self._test_pyfile("docs/examples/pyfile-example2.py", "My App") 67 | os.unlink("pyfile-example.py") 68 | os.unlink("conf.py") 69 | os.unlink("pyfile-example2.py") 70 | os.unlink("defaults.py") 71 | -------------------------------------------------------------------------------- /tests/test_layeredconfig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals 4 | """ 5 | test_layeredconfig 6 | ---------------------------------- 7 | 8 | Tests for `layeredconfig` module. 9 | """ 10 | 11 | 12 | import os 13 | import logging 14 | import sys 15 | import codecs 16 | from six import text_type as str 17 | from datetime import date, datetime 18 | import argparse 19 | import json 20 | from operator import itemgetter 21 | from copy import deepcopy 22 | try: 23 | from collections import OrderedDict 24 | except ImportError: # pragma: no cover 25 | # if on python 2.6 26 | from ordereddict import OrderedDict 27 | 28 | if sys.version_info < (2, 7, 0): # pragma: no cover 29 | import unittest2 as unittest 30 | else: 31 | import unittest 32 | import requests 33 | # The system under test 34 | from layeredconfig import (LayeredConfig, Defaults, INIFile, JSONFile, 35 | YAMLFile, PListFile, PyFile, Environment, 36 | Commandline, EtcdStore, UNIT_SEP) 37 | 38 | 39 | class LayeredConfigHelperTests(object): 40 | 41 | # Testcases for less-capable sources may override this 42 | supported_types = (str, int, bool, list, date, datetime) 43 | supports_nesting = True 44 | 45 | def _test_config_singlesection(self, cfg): 46 | self.assertIs(type(cfg.home), str) 47 | self.assertEqual(cfg.home, 'mydata') 48 | 49 | int_type = int if int in self.supported_types else str 50 | self.assertIs(type(cfg.processes), int_type) 51 | self.assertEqual(cfg.processes, int_type(4)) 52 | 53 | if bool in self.supported_types: 54 | self.assertEqual(cfg.force, True) 55 | else: 56 | self.assertEqual(cfg.force, "True") 57 | 58 | if list in self.supported_types: 59 | list_type = list 60 | list_want = ['foo', 'bar'] 61 | self.assertIs(type(cfg.extra[0]), str) 62 | else: 63 | list_type = str 64 | list_want = "foo, bar" # recommended list serialization 65 | self.assertIs(type(cfg.extra), list_type) 66 | self.assertEqual(cfg.extra, list_want) 67 | 68 | if date in self.supported_types: 69 | date_type = date 70 | date_want = date(2014, 10, 15) 71 | else: 72 | date_type = str 73 | date_want = "2014-10-15" # recommended date serialization 74 | self.assertIs(type(cfg.expires), date_type) 75 | self.assertEqual(cfg.expires, date_want) 76 | 77 | if datetime in self.supported_types: 78 | datetime_type = datetime 79 | datetime_want = datetime(2014, 10, 15, 14, 32, 7) 80 | else: 81 | datetime_type = str 82 | datetime_want = "2014-10-15 14:32:07" 83 | self.assertIs(type(cfg.lastrun), datetime_type) 84 | self.assertEqual(cfg.lastrun, datetime_want) 85 | 86 | def _test_config_subsections(self, cfg): 87 | self.assertEqual(cfg.home, 'mydata') 88 | with self.assertRaises(AttributeError): 89 | cfg.mymodule.home 90 | 91 | int_type = int if int in self.supported_types else str 92 | self.assertEqual(cfg.processes, int_type(4)) 93 | with self.assertRaises(AttributeError): 94 | cfg.mymodule.processes 95 | 96 | if bool in self.supported_types: 97 | self.assertEqual(cfg.mymodule.force, False) 98 | else: 99 | self.assertEqual(cfg.mymodule.force, "False") 100 | 101 | if list in self.supported_types: 102 | list_type = list 103 | list_want = ['foo', 'bar'] 104 | list_want_sub = ['foo', 'baz'] 105 | else: 106 | list_type = str 107 | list_want = "foo, bar" 108 | list_want_sub = "foo, baz" 109 | self.assertEqual(cfg.extra, list_want) 110 | self.assertEqual(cfg.mymodule.extra, list_want_sub) 111 | 112 | if self.supports_nesting: 113 | self.assertEqual(cfg.mymodule.arbitrary.nesting.depth, 'works') 114 | 115 | with self.assertRaises(AttributeError): 116 | cfg.expires 117 | 118 | if date in self.supported_types: 119 | date_type = date 120 | date_want = date(2014, 10, 15) 121 | else: 122 | date_type = str 123 | date_want = "2014-10-15" # recommended date serialization 124 | self.assertEqual(cfg.mymodule.expires, date_want) 125 | 126 | def _test_layered_configs(self, cfg): 127 | self.assertEqual(cfg.home, 'otherdata') 128 | 129 | bool_type = bool if bool in self.supported_types else str 130 | self.assertEqual(cfg.mymodule.force, bool_type(False)) 131 | 132 | def _test_layered_subsection_configs(self, cfg): 133 | if date in self.supported_types: 134 | date_type = date 135 | date_want = date(2014, 10, 15) 136 | else: 137 | date_type = str 138 | date_want = "2014-10-15" # recommended date serialization 139 | self.assertIs(type(cfg.mymodule.expires), date_type) 140 | self.assertEqual(cfg.mymodule.expires, date_want) 141 | 142 | 143 | class ConfigSourceHelperTests(LayeredConfigHelperTests): 144 | 145 | # First, a number of straightforward tests for any 146 | # ConfigSource-derived object. Concrete test classes should set up 147 | # self.simple and self.complex instances to match these. 148 | def test_keys(self): 149 | self.assertEqual(set(self.simple.keys()), 150 | set(('home', 'processes', 'force', 151 | 'extra', 'expires', 'lastrun'))) 152 | self.assertEqual(set(self.complex.keys()), 153 | set(('home', 'processes', 'force', 'extra'))) 154 | 155 | def test_subsection_keys(self): 156 | self.assertEqual(set(self.complex.subsection('mymodule').keys()), 157 | set(('force', 'extra', 'expires'))) 158 | 159 | def test_subsections(self): 160 | self.assertEqual(set(self.simple.subsections()), 161 | set()) 162 | self.assertEqual(set(self.complex.subsections()), 163 | set(('mymodule', 'extramodule'))) 164 | 165 | def test_subsection_nested(self): 166 | subsec = self.complex.subsection('mymodule') 167 | self.assertEqual(set(subsec.subsections()), 168 | set(('arbitrary',))) 169 | 170 | def test_has(self): 171 | for key in self.simple.keys(): 172 | self.assertTrue(self.simple.has(key)) 173 | 174 | def test_typed(self): 175 | for key in self.simple.keys(): 176 | self.assertTrue(self.simple.typed(key)) 177 | 178 | def test_get(self): 179 | # FIXME: This test should be able to look at supported_types 180 | # like test_singlesection and test_subsections do, so derived 181 | # testcase classes don't need to override it. 182 | self.assertEqual(self.simple.get("home"), "mydata") 183 | self.assertEqual(self.simple.get("processes"), 4) 184 | self.assertEqual(self.simple.get("force"), True) 185 | self.assertEqual(self.simple.get("extra"), ['foo', 'bar']) 186 | self.assertEqual(self.simple.get("expires"), date(2014, 10, 15)) 187 | self.assertEqual(self.simple.get("lastrun"), 188 | datetime(2014, 10, 15, 14, 32, 7)) 189 | 190 | # Then, two validation helpers for checking a complete 191 | # LayeredConfig object, where validation can be performed 192 | # different depending on the abilities of the source (eg. typing) 193 | def test_config_singlesection(self): 194 | # each subclass is responsible for creating a self.simple 195 | # object of the type being tested 196 | cfg = LayeredConfig(self.simple) 197 | self._test_config_singlesection(cfg) 198 | 199 | def test_config_subsections(self): 200 | cfg = LayeredConfig(self.complex) 201 | self._test_config_subsections(cfg) 202 | 203 | def test_layered_subsections(self): 204 | # this testcases excercies a bug (or rather a deficency in the 205 | # file-based sources) -- if the highest-priority source has 206 | # subsections, and a lower-priority file-based source lacks 207 | # those subsections, bad things would happen. 208 | # 209 | # see https://github.com/staffanm/layeredconfig/issues/2 210 | cfg = LayeredConfig(self.complex, self.extra) 211 | self._test_layered_configs(cfg) 212 | 213 | def test_overwriting_with_missing_subsections(self): 214 | # this testcase tests the inverse of test_layered_subsections 215 | cfg = LayeredConfig(self.complex, self.extra_layered) 216 | self._test_layered_subsection_configs(cfg) 217 | 218 | DUMP_DEFAULTS = {'processes': int, 219 | 'force': bool, 220 | 'extra': list, 221 | 'mymodule': { 222 | 'force': bool, 223 | 'extra': list, 224 | 'expires': date, 225 | }, 226 | 'extramodule': { 227 | 'unique': bool 228 | } 229 | } 230 | DUMP_WANT = {'home': 'mydata', 231 | 'processes': 4, 232 | 'force': True, 233 | 'extra': ['foo', 'bar'], 234 | 'mymodule': { 235 | 'force': False, 236 | 'extra': ['foo', 'baz'], 237 | 'expires': date(2014, 10, 15), 238 | 'arbitrary': { 239 | 'nesting': { 240 | 'depth': 'works' 241 | } 242 | } 243 | }, 244 | 'extramodule': { 245 | 'unique': True 246 | } 247 | } 248 | def test_dump(self): 249 | want = deepcopy(self.DUMP_WANT) 250 | if not self.supports_nesting: 251 | # INIFile does not support nested sections 252 | del want['mymodule']['arbitrary'] 253 | cfg = LayeredConfig(Defaults(self.DUMP_DEFAULTS), self.complex) 254 | got = LayeredConfig.dump(cfg) 255 | self.maxDiff = None 256 | self.assertEquals(want, got) 257 | 258 | 259 | def test_dump_layered(self): 260 | want = deepcopy(self.DUMP_WANT) 261 | if not self.supports_nesting: 262 | # INIFile does not support nested sections 263 | del want['mymodule']['arbitrary'] 264 | want['home'] = 'otherdata' 265 | cfg = LayeredConfig(Defaults(self.DUMP_DEFAULTS), self.complex, self.extra) 266 | got = LayeredConfig.dump(cfg) 267 | self.maxDiff = None 268 | self.assertEquals(want, got) 269 | 270 | 271 | 272 | # common helper 273 | class TestINIFileHelper(object): 274 | 275 | def setUp(self): 276 | super(TestINIFileHelper, self).setUp() 277 | with open("simple.ini", "w") as fp: 278 | fp.write(""" 279 | [__root__] 280 | home = mydata 281 | processes = 4 282 | force = True 283 | extra = foo, bar 284 | expires = 2014-10-15 285 | lastrun = 2014-10-15 14:32:07 286 | """) 287 | 288 | with open("complex.ini", "w") as fp: 289 | fp.write(""" 290 | [__root__] 291 | home = mydata 292 | processes = 4 293 | force = True 294 | extra = foo, bar 295 | 296 | [mymodule] 297 | force = False 298 | extra = foo, baz 299 | expires = 2014-10-15 300 | 301 | [mymodule.arbitrary.nesting] 302 | depth = works 303 | 304 | [extramodule] 305 | unique = True 306 | """) 307 | 308 | with open("extra.ini", "w") as fp: 309 | fp.write(""" 310 | [__root__] 311 | home = otherdata 312 | """) 313 | 314 | with open("extra-layered.ini", "w") as fp: 315 | fp.write(""" 316 | [mymodule] 317 | expires = 2014-10-15 318 | """) 319 | 320 | 321 | 322 | def tearDown(self): 323 | super(TestINIFileHelper, self).tearDown() 324 | os.unlink("simple.ini") 325 | os.unlink("complex.ini") 326 | os.unlink("extra.ini") 327 | os.unlink("extra-layered.ini") 328 | 329 | 330 | class TestDefaults(unittest.TestCase, ConfigSourceHelperTests): 331 | 332 | simple = Defaults({'home': 'mydata', 333 | 'processes': 4, 334 | 'force': True, 335 | 'extra': ['foo', 'bar'], 336 | 'expires': date(2014, 10, 15), 337 | 'lastrun': datetime(2014, 10, 15, 14, 32, 7)}) 338 | 339 | complex = Defaults({'home': 'mydata', 340 | 'processes': 4, 341 | 'force': True, 342 | 'extra': ['foo', 'bar'], 343 | 'mymodule': {'force': False, 344 | 'extra': ['foo', 'baz'], 345 | 'expires': date(2014, 10, 15), 346 | 'arbitrary': { 347 | 'nesting': { 348 | 'depth': 'works' 349 | } 350 | } 351 | }, 352 | 'extramodule': {'unique': True}}) 353 | 354 | extra = Defaults({'home': 'otherdata'}) 355 | 356 | extra_layered = Defaults({'mymodule': {'force': True}}) 357 | 358 | class TestINIFile(TestINIFileHelper, unittest.TestCase, 359 | ConfigSourceHelperTests): 360 | 361 | supported_types = (str,) 362 | supports_nesting = True 363 | 364 | def setUp(self): 365 | super(TestINIFile, self).setUp() 366 | self.simple = INIFile("simple.ini") 367 | self.complex = INIFile("complex.ini") 368 | self.extra = INIFile("extra.ini") 369 | self.extra_layered = INIFile("extra-layered.ini") 370 | 371 | # Overrides of TestHelper.test_get, .test_typed and 372 | # .test_subsection_nested due to limitations of INIFile 373 | # INIFile carries no typing information 374 | def test_get(self): 375 | self.assertEqual(self.simple.get("home"), "mydata") 376 | self.assertEqual(self.simple.get("processes"), "4") 377 | self.assertEqual(self.simple.get("force"), "True") 378 | self.assertEqual(self.simple.get("extra"), "foo, bar") 379 | self.assertEqual(self.simple.get("expires"), "2014-10-15") 380 | self.assertEqual(self.simple.get("lastrun"), "2014-10-15 14:32:07") 381 | 382 | def test_typed(self): 383 | for key in self.simple.keys(): 384 | self.assertFalse(self.simple.typed(key)) 385 | 386 | def test_inifile_default_as_root(self): 387 | # using a rootsection named DEFAULT triggers different 388 | # cascading-like behaviour in configparser. 389 | 390 | # load a modified version of complex.ini 391 | with open("complex.ini") as fp: 392 | ini = fp.read() 393 | 394 | with open("complex-otherroot.ini", "w") as fp: 395 | fp.write(ini.replace("[__root__]", "[DEFAULT]")) 396 | cfg = LayeredConfig(INIFile("complex-otherroot.ini", 397 | rootsection="DEFAULT")) 398 | 399 | # this is a modified/simplified version of ._test_subsections 400 | self.assertEqual(cfg.home, 'mydata') 401 | self.assertEqual(cfg.processes, '4') 402 | self.assertEqual(cfg.force, 'True') 403 | self.assertEqual(cfg.mymodule.force, 'False') 404 | self.assertEqual(cfg.extra, "foo, bar") 405 | self.assertEqual(cfg.mymodule.extra, "foo, baz") 406 | with self.assertRaises(AttributeError): 407 | cfg.expires 408 | self.assertEqual(cfg.mymodule.expires, "2014-10-15") 409 | 410 | # this is really unwanted cascading behaviour 411 | self.assertEqual(cfg.mymodule.home, 'mydata') 412 | self.assertEqual(cfg.mymodule.processes, '4') 413 | 414 | os.unlink("complex-otherroot.ini") 415 | 416 | def test_inifile_nonexistent(self): 417 | logging.getLogger().setLevel(logging.CRITICAL) 418 | cfg = LayeredConfig(INIFile("nonexistent.ini")) 419 | self.assertEqual([], list(cfg)) 420 | 421 | # make sure a nonexistent inifile doesn't interfere with the 422 | # rest of the LayeredConfig object 423 | defobj = Defaults({'datadir': 'something'}) 424 | iniobj = INIFile("nonexistent.ini") 425 | cfg = LayeredConfig(defobj, iniobj) 426 | self.assertEqual("something", cfg.datadir) 427 | 428 | # and make sure it's settable (should set up the INIFile 429 | # object and affect it, and leave the defaults dict untouched 430 | # as it's the lowest priority) 431 | cfg.datadir = "else" 432 | self.assertEqual("else", cfg.datadir) 433 | self.assertEqual("else", iniobj.get("datadir")) 434 | self.assertEqual("something", defobj.get("datadir")) 435 | 436 | # same as above, but with a "empty" INIFile object 437 | iniobj = INIFile() 438 | cfg = LayeredConfig(defobj, iniobj) 439 | self.assertEqual("something", cfg.datadir) 440 | cfg.datadir = "else" 441 | self.assertEqual("else", cfg.datadir) 442 | 443 | def test_write(self): 444 | cfg = LayeredConfig(INIFile("complex.ini")) 445 | cfg.mymodule.expires = date(2014, 10, 24) 446 | cfg.mymodule.extra = ['foo', 'baz', 'quux'] 447 | # calling write for any submodule will force a write of the 448 | # entire config file 449 | LayeredConfig.write(cfg.mymodule) 450 | want = """[__root__] 451 | home = mydata 452 | processes = 4 453 | force = True 454 | extra = foo, bar 455 | 456 | [mymodule] 457 | force = False 458 | extra = foo, baz, quux 459 | expires = 2014-10-24 460 | 461 | [mymodule.arbitrary.nesting] 462 | depth = works 463 | 464 | [extramodule] 465 | unique = True 466 | 467 | """ 468 | with open("complex.ini") as fp: 469 | got = fp.read().replace("\r\n", "\n") 470 | self.assertEqual(want, got) 471 | 472 | 473 | class TestJSONFile(unittest.TestCase, ConfigSourceHelperTests): 474 | 475 | supported_types = (str, int, bool, list) 476 | 477 | def setUp(self): 478 | with open("simple.json", "w") as fp: 479 | fp.write(""" 480 | {"home": "mydata", 481 | "processes": 4, 482 | "force": true, 483 | "extra": ["foo", "bar"], 484 | "expires": "2014-10-15", 485 | "lastrun": "2014-10-15 14:32:07"} 486 | """) 487 | 488 | with open("complex.json", "w") as fp: 489 | fp.write(""" 490 | {"home": "mydata", 491 | "processes": 4, 492 | "force": true, 493 | "extra": ["foo", "bar"], 494 | "mymodule": {"force": false, 495 | "extra": ["foo", "baz"], 496 | "expires": "2014-10-15", 497 | "arbitrary": { 498 | "nesting": { 499 | "depth": "works" 500 | } 501 | } 502 | }, 503 | "extramodule": {"unique": true} 504 | } 505 | """) 506 | with open("extra.json", "w") as fp: 507 | fp.write('{"home": "otherdata"}') 508 | 509 | with open("extra-layered.json", "w") as fp: 510 | fp.write('{"mymodule": {"force": true}}') 511 | 512 | self.simple = JSONFile("simple.json") 513 | self.complex = JSONFile("complex.json") 514 | self.extra = JSONFile("extra.json") 515 | self.extra_layered = JSONFile("extra-layered.json") 516 | 517 | def tearDown(self): 518 | os.unlink("simple.json") 519 | os.unlink("complex.json") 520 | os.unlink("extra.json") 521 | os.unlink("extra-layered.json") 522 | 523 | def test_get(self): 524 | self.assertEqual(self.simple.get("home"), "mydata") 525 | self.assertEqual(self.simple.get("processes"), 4) 526 | self.assertEqual(self.simple.get("force"), True) 527 | self.assertEqual(self.simple.get("extra"), ['foo', 'bar']) 528 | self.assertEqual(self.simple.get("expires"), "2014-10-15") 529 | self.assertEqual(self.simple.get("lastrun"), "2014-10-15 14:32:07") 530 | 531 | def test_typed(self): 532 | for key in self.simple.keys(): 533 | # JSON can type ints, bools and lists 534 | if key in ("processes", "force", "extra"): 535 | self.assertTrue(self.simple.typed(key)) 536 | else: 537 | self.assertFalse(self.simple.typed(key)) 538 | 539 | def test_write(self): 540 | self.maxDiff = None 541 | cfg = LayeredConfig(self.complex) 542 | cfg.mymodule.expires = date(2014, 10, 24) 543 | cfg.mymodule.extra.append("quux") 544 | # calling write for any submodule will force a write of the 545 | # entire config file 546 | LayeredConfig.write(cfg.mymodule) 547 | want = """{ 548 | "extra": [ 549 | "foo", 550 | "bar" 551 | ], 552 | "extramodule": { 553 | "unique": true 554 | }, 555 | "force": true, 556 | "home": "mydata", 557 | "mymodule": { 558 | "arbitrary": { 559 | "nesting": { 560 | "depth": "works" 561 | } 562 | }, 563 | "expires": "2014-10-24", 564 | "extra": [ 565 | "foo", 566 | "baz", 567 | "quux" 568 | ], 569 | "force": false 570 | }, 571 | "processes": 4 572 | }""" 573 | with open("complex.json") as fp: 574 | got = fp.read().replace("\r\n", "\n") 575 | self.assertEqual(want, got) 576 | 577 | class TestYAMLFile(unittest.TestCase, 578 | ConfigSourceHelperTests): 579 | def setUp(self): 580 | with open("simple.yaml", "w") as fp: 581 | fp.write(""" 582 | home: mydata 583 | processes: 4 584 | force: true 585 | extra: 586 | - foo 587 | - bar 588 | expires: 2014-10-15 589 | lastrun: 2014-10-15 14:32:07 590 | """) 591 | with open("complex.yaml", "w") as fp: 592 | fp.write(""" 593 | home: mydata 594 | processes: 4 595 | force: true 596 | extra: 597 | - foo 598 | - bar 599 | mymodule: 600 | force: false 601 | extra: 602 | - foo 603 | - baz 604 | expires: 2014-10-15 605 | arbitrary: 606 | nesting: 607 | depth: works 608 | extramodule: 609 | unique: true 610 | """) 611 | with open("extra.yaml", "w") as fp: 612 | fp.write(""" 613 | home: otherdata 614 | """) 615 | 616 | with open("extra-layered.yaml", "w") as fp: 617 | fp.write(""" 618 | mymodule: 619 | force: true 620 | """) 621 | 622 | self.simple = YAMLFile("simple.yaml") 623 | self.complex = YAMLFile("complex.yaml") 624 | self.extra = YAMLFile("extra.yaml") 625 | self.extra_layered = YAMLFile("extra-layered.yaml") 626 | 627 | def tearDown(self): 628 | os.unlink("simple.yaml") 629 | os.unlink("complex.yaml") 630 | os.unlink("extra.yaml") 631 | os.unlink("extra-layered.yaml") 632 | 633 | # Also, strings are unicode when they need to be, 634 | # str otherwise. 635 | def test_i18n(self): 636 | with codecs.open("i18n.yaml", "w", encoding="utf-8") as fp: 637 | fp.write("shrimpsandwich: Räksmörgås") 638 | cfg = LayeredConfig(YAMLFile("i18n.yaml")) 639 | self.assertEqual("Räksmörgås", cfg.shrimpsandwich) 640 | os.unlink("i18n.yaml") 641 | 642 | def test_write(self): 643 | cfg = LayeredConfig(self.complex) 644 | cfg.mymodule.expires = date(2014, 10, 24) 645 | cfg.mymodule.extra.append('quux') 646 | # calling write for any submodule will force a write of the 647 | # entire config file 648 | LayeredConfig.write(cfg.mymodule) 649 | # note that pyyaml sorts keys alphabetically and has specific 650 | # ideas on how to format the result (controllable through 651 | # mostly-undocumented args to dump()) 652 | want = """ 653 | extra: 654 | - foo 655 | - bar 656 | extramodule: 657 | unique: true 658 | force: true 659 | home: mydata 660 | mymodule: 661 | arbitrary: 662 | nesting: 663 | depth: works 664 | expires: 2014-10-24 665 | extra: 666 | - foo 667 | - baz 668 | - quux 669 | force: false 670 | processes: 4 671 | """.lstrip() 672 | with open("complex.yaml") as fp: 673 | got = fp.read().replace("\r\n", "\n") 674 | self.assertEqual(want, got) 675 | 676 | 677 | class TestPListFile(unittest.TestCase, ConfigSourceHelperTests): 678 | 679 | supported_types = (str, int, bool, list, datetime) 680 | 681 | def setUp(self): 682 | with open("simple.plist", "w") as fp: 683 | fp.write(""" 684 | 685 | 686 | 687 | expires 688 | 2014-10-15 689 | extra 690 | 691 | foo 692 | bar 693 | 694 | force 695 | 696 | home 697 | mydata 698 | lastrun 699 | 2014-10-15T14:32:07Z 700 | processes 701 | 4 702 | 703 | 704 | """) 705 | with open("complex.plist", "w") as fp: 706 | fp.write(""" 707 | 708 | 709 | 710 | extra 711 | 712 | foo 713 | bar 714 | 715 | extramodule 716 | 717 | unique 718 | 719 | 720 | force 721 | 722 | home 723 | mydata 724 | mymodule 725 | 726 | arbitrary 727 | 728 | nesting 729 | 730 | depth 731 | works 732 | 733 | 734 | expires 735 | 2014-10-15 736 | extra 737 | 738 | foo 739 | baz 740 | 741 | force 742 | 743 | 744 | processes 745 | 4 746 | 747 | 748 | """) 749 | with open("extra.plist", "w") as fp: 750 | fp.write(""" 751 | 752 | 753 | 754 | home 755 | otherdata 756 | 757 | 758 | """) 759 | 760 | with open("extra-layered.plist", "w") as fp: 761 | fp.write(""" 762 | 763 | 764 | 765 | mymodule 766 | 767 | force 768 | 769 | 770 | 771 | 772 | """) 773 | self.simple = PListFile("simple.plist") 774 | self.complex = PListFile("complex.plist") 775 | self.extra = PListFile("extra.plist") 776 | self.extra_layered = PListFile("extra-layered.plist") 777 | 778 | def tearDown(self): 779 | os.unlink("simple.plist") 780 | os.unlink("complex.plist") 781 | os.unlink("extra.plist") 782 | os.unlink("extra-layered.plist") 783 | 784 | # override only because plists cannot handle date objects (only datetime) 785 | def test_get(self): 786 | self.assertEqual(self.simple.get("home"), "mydata") 787 | self.assertEqual(self.simple.get("processes"), 4) 788 | self.assertEqual(self.simple.get("force"), True) 789 | self.assertEqual(self.simple.get("extra"), ['foo', 'bar']) 790 | self.assertEqual(self.simple.get("expires"), "2014-10-15") 791 | self.assertEqual(self.simple.get("lastrun"), 792 | datetime(2014, 10, 15, 14, 32, 7)) 793 | 794 | def test_write(self): 795 | self.maxDiff = None 796 | cfg = LayeredConfig(self.complex) 797 | cfg.mymodule.expires = date(2014, 10, 24) 798 | cfg.mymodule.extra.append('quux') 799 | # calling write for any submodule will force a write of the 800 | # entire config file 801 | LayeredConfig.write(cfg.mymodule) 802 | # note: plistlib creates files with tabs, not spaces. 803 | want = """ 804 | 805 | 806 | 807 | extra 808 | 809 | foo 810 | bar 811 | 812 | extramodule 813 | 814 | unique 815 | 816 | 817 | force 818 | 819 | home 820 | mydata 821 | mymodule 822 | 823 | arbitrary 824 | 825 | nesting 826 | 827 | depth 828 | works 829 | 830 | 831 | expires 832 | 2014-10-24 833 | extra 834 | 835 | foo 836 | baz 837 | quux 838 | 839 | force 840 | 841 | 842 | processes 843 | 4 844 | 845 | 846 | """ 847 | if sys.version_info < (2,7,0): # pragma: no cover 848 | # on py26, the doctype includes "Apple Computer" not "Apple"... 849 | want = want.replace("//Apple//", "//Apple Computer//") 850 | with open("complex.plist") as fp: 851 | got = fp.read().replace("\r\n", "\n") 852 | self.assertEqual(want, got) 853 | 854 | def test_typed(self): 855 | for key in self.simple.keys(): 856 | # PList can type ints, bools, lists and datetimes (but not dates) 857 | if key in ("processes", "force", "extra", "lastrun"): 858 | self.assertTrue(self.simple.typed(key)) 859 | else: 860 | self.assertFalse(self.simple.typed(key)) 861 | 862 | 863 | class TestPyFile(unittest.TestCase, ConfigSourceHelperTests): 864 | 865 | def setUp(self): 866 | with open("simple.py", "w") as fp: 867 | fp.write("""from __future__ import unicode_literals 868 | import datetime 869 | 870 | expires = datetime.date(2014,10,15) 871 | extra = ['foo', 'bar'] 872 | force = True 873 | home = 'mydata' 874 | lastrun = datetime.datetime(2014,10,15,14,32,7) 875 | processes = 4 876 | """) 877 | with open("complex.py", "w") as fp: 878 | fp.write("""from __future__ import unicode_literals 879 | import datetime 880 | 881 | extra = ['foo', 'bar'] 882 | force = True 883 | home = 'mydata' 884 | processes = 4 885 | 886 | mymodule = Subsection() 887 | mymodule.expires = datetime.date(2014,10,15) 888 | mymodule.extra = ['foo', 'baz'] 889 | mymodule.force = False 890 | mymodule.arbitrary = Subsection() 891 | mymodule.arbitrary.nesting = Subsection() 892 | mymodule.arbitrary.nesting.depth = 'works' 893 | 894 | extramodule = Subsection() 895 | extramodule.unique = True 896 | """) 897 | 898 | with open("extra.py", "w") as fp: 899 | fp.write("""from __future__ import unicode_literals 900 | 901 | home = 'otherdata' 902 | """) 903 | 904 | with open("extra-layered.py", "w") as fp: 905 | fp.write("""from __future__ import unicode_literals 906 | 907 | mymodule = Subsection() 908 | mymodule.force = True 909 | """) 910 | self.simple = PyFile("simple.py") 911 | self.complex = PyFile("complex.py") 912 | self.extra = PyFile("extra.py") 913 | self.extra_layered = PyFile("extra-layered.py") 914 | 915 | def tearDown(self): 916 | os.unlink("simple.py") 917 | os.unlink("complex.py") 918 | os.unlink("extra.py") 919 | os.unlink("extra-layered.py") 920 | 921 | 922 | class TestCommandline(unittest.TestCase, ConfigSourceHelperTests): 923 | 924 | # Note: bool is "half-way" supported. Only value-less parameters 925 | # are typed as bool (eg "--force", not "--force=True") 926 | supported_types = (str, list, bool) 927 | 928 | simple_cmdline = ['--home=mydata', 929 | '--processes=4', 930 | '--force', # note implicit boolean typing 931 | '--extra=foo', 932 | '--extra=bar', 933 | '--expires=2014-10-15', 934 | '--lastrun=2014-10-15 14:32:07'] 935 | 936 | complex_cmdline = ['--home=mydata', 937 | '--processes=4', 938 | '--force=True', 939 | '--extra=foo', 940 | '--extra=bar', 941 | '--mymodule-force=False', 942 | '--mymodule-extra=foo', 943 | '--mymodule-extra=baz', 944 | '--mymodule-expires=2014-10-15', 945 | '--mymodule-arbitrary-nesting-depth=works', 946 | '--extramodule-unique'] 947 | 948 | def setUp(self): 949 | super(TestCommandline, self).setUp() 950 | # this means we lack typing information 951 | self.simple = Commandline(self.simple_cmdline) 952 | self.complex = Commandline(self.complex_cmdline) 953 | 954 | 955 | # Overrides of TestHelper.test_get, .test_typed and and due to 956 | # limitations of Commandline (carries almost no typeinfo) 957 | def test_get(self): 958 | self.assertEqual(self.simple.get("home"), "mydata") 959 | self.assertEqual(self.simple.get("processes"), "4") 960 | self.assertEqual(self.simple.get("force"), True) 961 | self.assertEqual(self.simple.get("extra"), ['foo','bar']) # note typed! 962 | self.assertEqual(self.simple.get("expires"), "2014-10-15") 963 | self.assertEqual(self.simple.get("lastrun"), "2014-10-15 14:32:07") 964 | 965 | def test_typed(self): 966 | for key in self.simple.keys(): 967 | # these should be typed as bool and list, respectively 968 | if key in ("force", "extra"): 969 | self.assertTrue(self.simple.typed(key)) 970 | else: 971 | self.assertFalse(self.simple.typed(key)) 972 | 973 | def test_config_subsections(self): 974 | # this case uses valued parameter for --force et al, which 975 | # cannot be reliably converted to bools using only intrinsic 976 | # information 977 | self.supported_types = (str, list) 978 | super(TestCommandline, self).test_config_subsections() 979 | 980 | def test_layered_subsections(self): 981 | pass # this test has no meaning for Command line arguments 982 | # (can't have two sets of them) 983 | 984 | def test_overwriting_with_missing_subsections(self): 985 | pass # this test has no meaning for Command line arguments 986 | # (can't have two sets of them) 987 | 988 | def test_dump_layered(self): 989 | pass 990 | 991 | def test_set(self): 992 | self.simple.set("home", "away from home") 993 | self.assertEqual(self.simple.get("home"), "away from home") 994 | 995 | def test_custom_sectionsep(self): 996 | # https://github.com/staffanm/layeredconfig/issues/9 -- we 997 | # want to make sure that cfg.db.host returns a simple string, 998 | # not a 1-element array of strings, even when not using the 999 | # default section separator '-'. 1000 | cmdline = ['--dbhost=string', '--db_host=array_of_strings'] 1001 | src = Commandline(cmdline, sectionsep="_") 1002 | cfg = LayeredConfig(src) 1003 | self.assertIsInstance(cfg.dbhost, str) 1004 | self.assertEquals(cfg.dbhost, 'string') 1005 | self.assertIsInstance(cfg.db.host, str) 1006 | self.assertEquals(cfg.db.host, 'array_of_strings') 1007 | 1008 | def test_custom_sectionsep_2(self): 1009 | # https://github.com/staffanm/layeredconfig/issues/13 1010 | cmdline = ['--log.rotation_size=100'] 1011 | src = Commandline(cmdline, sectionsep=".") 1012 | cfg = LayeredConfig(src) 1013 | self.assertIsInstance(cfg.log.rotation_size, str) 1014 | self.assertEquals(cfg.log.rotation_size, '100') 1015 | 1016 | 1017 | class TestCommandlineConfigured(TestCommandline): 1018 | 1019 | supported_types = (str, int, bool, date, datetime, list) 1020 | def setUp(self): 1021 | super(TestCommandlineConfigured, self).setUp() 1022 | simp = argparse.ArgumentParser(description="This is a simple program") 1023 | simp.add_argument('--home', help="The home directory of the app") 1024 | simp.add_argument('--processes', type=int, help="Number of simultaneous processes") 1025 | simp.add_argument('--force', type=LayeredConfig.boolconvert, nargs='?', const=True) 1026 | simp.add_argument('--extra', action='append') 1027 | simp.add_argument('--expires', type=LayeredConfig.dateconvert) 1028 | simp.add_argument('--lastrun', type=LayeredConfig.datetimeconvert) 1029 | simp.add_argument('--unused') 1030 | self.simple = Commandline(self.simple_cmdline, 1031 | parser=simp) 1032 | 1033 | comp = argparse.ArgumentParser(description="This is a complex program") 1034 | comp.add_argument('--home', help="The home directory of the app") 1035 | comp.add_argument('--processes', type=int, help="Number of simultaneous processes") 1036 | comp.add_argument('--force', type=LayeredConfig.boolconvert, nargs='?', const=True) 1037 | comp.add_argument('--extra', action='append') 1038 | comp.add_argument('--mymodule-force', type=LayeredConfig.boolconvert, nargs='?', const=True, dest='mymodule'+UNIT_SEP+'force') 1039 | comp.add_argument('--mymodule-extra', action='append', dest='mymodule'+UNIT_SEP+'extra') 1040 | comp.add_argument('--mymodule-expires', type=LayeredConfig.dateconvert, dest='mymodule'+UNIT_SEP+'expires') 1041 | comp.add_argument('--mymodule-arbitrary-nesting-depth', dest='mymodule'+UNIT_SEP+'arbitrary'+UNIT_SEP+"nesting"+UNIT_SEP+"depth") 1042 | comp.add_argument('--extramodule-unique', nargs='?', const=True, dest='extramodule'+UNIT_SEP+'unique') 1043 | self.complex = Commandline(self.complex_cmdline, 1044 | parser=comp) 1045 | 1046 | def test_get(self): 1047 | # re-enable the original impl of test_get 1048 | ConfigSourceHelperTests.test_get(self) 1049 | 1050 | def test_config_subsections(self): 1051 | # re-enable the original impl of test_config_subsections 1052 | ConfigSourceHelperTests.test_config_subsections(self) 1053 | 1054 | def test_typed(self): 1055 | # re-enable the original impl of test_get 1056 | ConfigSourceHelperTests.test_typed(self) 1057 | 1058 | 1059 | class TestEnvironment(unittest.TestCase, ConfigSourceHelperTests): 1060 | 1061 | supported_types = (str,) 1062 | 1063 | simple = Environment({'MYAPP_HOME': 'mydata', 1064 | 'MYAPP_PROCESSES': '4', 1065 | 'MYAPP_FORCE': 'True', 1066 | 'MYAPP_EXTRA': 'foo, bar', 1067 | 'MYAPP_EXPIRES': '2014-10-15', 1068 | 'MYAPP_LASTRUN': '2014-10-15 14:32:07'}, 1069 | prefix="MYAPP_") 1070 | complex = Environment({'MYAPP_HOME': 'mydata', 1071 | 'MYAPP_PROCESSES': '4', 1072 | 'MYAPP_FORCE': 'True', 1073 | 'MYAPP_EXTRA': 'foo, bar', 1074 | 'MYAPP_MYMODULE_FORCE': 'False', 1075 | 'MYAPP_MYMODULE_EXTRA': "foo, baz", 1076 | 'MYAPP_MYMODULE_EXPIRES': '2014-10-15', 1077 | 'MYAPP_MYMODULE_ARBITRARY_NESTING_DEPTH': 'works', 1078 | 'MYAPP_EXTRAMODULE_UNIQUE': 'True'}, 1079 | prefix="MYAPP_") 1080 | 1081 | def test_get(self): 1082 | self.assertEqual(self.simple.get("home"), "mydata") 1083 | self.assertEqual(self.simple.get("processes"), "4") 1084 | self.assertEqual(self.simple.get("force"), "True") 1085 | self.assertEqual(self.simple.get("extra"), "foo, bar") 1086 | self.assertEqual(self.simple.get("expires"), "2014-10-15") 1087 | self.assertEqual(self.simple.get("lastrun"), "2014-10-15 14:32:07") 1088 | 1089 | def test_layered_subsections(self): 1090 | pass # this test has no meaning for environment variables 1091 | # (can't have two sets of them) 1092 | 1093 | def test_overwriting_with_missing_subsections(self): 1094 | pass # this test has no meaning for environment variables 1095 | # (can't have two sets of them) 1096 | 1097 | def test_dump_layered(self): 1098 | pass 1099 | 1100 | def test_typed(self): 1101 | for key in self.simple.keys(): 1102 | self.assertFalse(self.simple.typed(key)) 1103 | 1104 | 1105 | # NB: This assumes that an etcd daemon is running with default 1106 | # settings 1107 | ETCD_BASE = "http://127.0.0.1:2379/v2/keys" 1108 | 1109 | @unittest.skipIf("APPVEYOR" in os.environ, 1110 | "Not running etcd dependent tests on Appveyor") 1111 | class TestEtcdStore(unittest.TestCase, ConfigSourceHelperTests): 1112 | maxDiff = None 1113 | def strlower(value): 1114 | return str(value).lower() 1115 | 1116 | supported_types = (str,) 1117 | 1118 | def _clear_server(self): 1119 | resp = requests.get(ETCD_BASE + "/") 1120 | resp.raise_for_status() 1121 | json = resp.json() 1122 | if 'nodes' in json['node']: 1123 | for node in json['node']['nodes']: 1124 | resp = requests.delete(ETCD_BASE + "%s?recursive=true" % node['key']) 1125 | 1126 | @property 1127 | def simple(self): 1128 | self._clear_server() 1129 | requests.put(ETCD_BASE + "/home", data={'value': 'mydata'}) 1130 | requests.put(ETCD_BASE + "/processes", data={'value': '4'}) 1131 | requests.put(ETCD_BASE + "/force", data={'value': "True"}) 1132 | requests.put(ETCD_BASE + "/extra", data={'value': "foo, bar"}) 1133 | requests.put(ETCD_BASE + "/expires", data={'value': "2014-10-15"}) 1134 | requests.put(ETCD_BASE + "/lastrun", data={'value': "2014-10-15 14:32:07"}) 1135 | return EtcdStore() 1136 | 1137 | @property 1138 | def complex(self): 1139 | self._clear_server() 1140 | requests.put(ETCD_BASE + "/home", data={'value': "mydata"}) 1141 | requests.put(ETCD_BASE + "/processes", data={'value': "4"}) 1142 | requests.put(ETCD_BASE + "/force", data={'value': "True"}) 1143 | requests.put(ETCD_BASE + "/extra", data={'value': "foo, bar"}) 1144 | requests.put(ETCD_BASE + "/mymodule/force", data={'value': "False"}) 1145 | requests.put(ETCD_BASE + "/mymodule/extra", data={'value': "foo, baz"}) 1146 | requests.put(ETCD_BASE + "/mymodule/expires", data={'value': "2014-10-15"}) 1147 | requests.put(ETCD_BASE + "/mymodule/arbitrary/nesting/depth", data={'value': "works"}) 1148 | requests.put(ETCD_BASE + "/extramodule/unique", data={'value': "True"}) 1149 | return EtcdStore() 1150 | 1151 | def test_layered_subsections(self): 1152 | pass # this test has no meaning for etcd stores, as a single 1153 | # server cannot have two sets of configuration to layere 1154 | # (however, we could concievably have two distinct 1155 | # servers -- but that isn't supported on ground of being 1156 | # too complicated) 1157 | 1158 | def test_overwriting_with_missing_subsections(self): 1159 | pass # ditto 1160 | 1161 | def test_dump_layered(self): 1162 | pass 1163 | 1164 | def test_typed(self): 1165 | for key in self.simple.keys(): 1166 | self.assertFalse(self.simple.typed(key)) 1167 | 1168 | def test_get(self): 1169 | # FIXME: This test should be able to look at supported_types 1170 | # like test_singlesection and test_subsections do, so derived 1171 | # testcase classes don't need to override it. 1172 | conf = self.simple 1173 | self.assertEqual(conf.get("home"), "mydata") 1174 | self.assertEqual(conf.get("processes"), "4") 1175 | self.assertEqual(conf.get("force"), "True") 1176 | self.assertEqual(conf.get("extra"), "foo, bar") 1177 | self.assertEqual(conf.get("expires"), "2014-10-15") 1178 | self.assertEqual(conf.get("lastrun"), "2014-10-15 14:32:07") 1179 | 1180 | def test_write(self): 1181 | def indexfilter(node): 1182 | # remove keys like createdIdex / modifiedIndex whose 1183 | # values always changes 1184 | if isinstance(node, dict): 1185 | for key in list(node.keys()): 1186 | if key == "nodes": 1187 | indexfilter(node[key]) 1188 | elif key.endswith("Index"): 1189 | del node[key] 1190 | else: 1191 | node[:] = sorted(node, key=itemgetter('key')) 1192 | for subnode in node: 1193 | indexfilter(subnode) 1194 | 1195 | cfg = LayeredConfig(self.complex) 1196 | cfg.extra = ['foo', 'value with space'] 1197 | cfg.mymodule.expires = date(2014, 10, 24) 1198 | cfg.mymodule.extra = ['foo', 'baz', 'quux'] 1199 | # note that this will write the entire config incl cfg.extra, 1200 | # not just the values in the 'mymodule' subsection. 1201 | LayeredConfig.write(cfg.mymodule) 1202 | want = """ 1203 | { 1204 | "dir": true, 1205 | "nodes": [ 1206 | { 1207 | "createdIndex": 4627, 1208 | "key": "/home", 1209 | "modifiedIndex": 4627, 1210 | "value": "mydata" 1211 | }, 1212 | { 1213 | "createdIndex": 4628, 1214 | "key": "/processes", 1215 | "modifiedIndex": 4628, 1216 | "value": "4" 1217 | }, 1218 | { 1219 | "createdIndex": 4629, 1220 | "key": "/force", 1221 | "modifiedIndex": 4629, 1222 | "value": "True" 1223 | }, 1224 | { 1225 | "createdIndex": 4630, 1226 | "key": "/extra", 1227 | "modifiedIndex": 4630, 1228 | "value": "['foo', 'value with space']" 1229 | }, 1230 | { 1231 | "createdIndex": 4631, 1232 | "dir": true, 1233 | "key": "/mymodule", 1234 | "modifiedIndex": 4631, 1235 | "nodes": [ 1236 | { 1237 | "createdIndex": 4631, 1238 | "key": "/mymodule/force", 1239 | "modifiedIndex": 4631, 1240 | "value": "False" 1241 | }, 1242 | { 1243 | "createdIndex": 4632, 1244 | "key": "/mymodule/extra", 1245 | "modifiedIndex": 4632, 1246 | "value": "foo, baz, quux" 1247 | }, 1248 | { 1249 | "createdIndex": 4633, 1250 | "key": "/mymodule/expires", 1251 | "modifiedIndex": 4633, 1252 | "value": "2014-10-24" 1253 | }, 1254 | { 1255 | "createdIndex": 4634, 1256 | "dir": true, 1257 | "key": "/mymodule/arbitrary", 1258 | "modifiedIndex": 4634, 1259 | "nodes": [ 1260 | { 1261 | "createdIndex": 4634, 1262 | "dir": true, 1263 | "key": "/mymodule/arbitrary/nesting", 1264 | "modifiedIndex": 4634, 1265 | "nodes": [ 1266 | { 1267 | "createdIndex": 4634, 1268 | "key": "/mymodule/arbitrary/nesting/depth", 1269 | "modifiedIndex": 4634, 1270 | "value": "works" 1271 | } 1272 | ] 1273 | } 1274 | ] 1275 | } 1276 | ] 1277 | }, 1278 | { 1279 | "createdIndex": 4635, 1280 | "dir": true, 1281 | "key": "/extramodule", 1282 | "modifiedIndex": 4635, 1283 | "nodes": [ 1284 | { 1285 | "createdIndex": 4635, 1286 | "key": "/extramodule/unique", 1287 | "modifiedIndex": 4635, 1288 | "value": "True" 1289 | } 1290 | ] 1291 | } 1292 | ] 1293 | }""" 1294 | want = json.loads(want) 1295 | indexfilter(want) 1296 | got = requests.get("http://localhost:2379/v2/keys/?recursive=true").json()['node'] 1297 | indexfilter(got) 1298 | self.assertEqual(want, got) 1299 | 1300 | 1301 | class TestTyping(unittest.TestCase, LayeredConfigHelperTests): 1302 | types = {'home': str, 1303 | 'processes': int, 1304 | 'force': bool, 1305 | 'extra': list, 1306 | 'mymodule': {'force': bool, 1307 | 'extra': list, 1308 | 'expires': date, 1309 | 'lastrun': datetime, 1310 | } 1311 | } 1312 | 1313 | def test_typed_commandline(self): 1314 | cmdline = ['--home=mydata', 1315 | '--processes=4', 1316 | '--force=True', 1317 | '--extra=foo', 1318 | '--extra=bar', 1319 | '--implicitboolean', 1320 | '--mymodule-force=False', 1321 | '--mymodule-extra=foo', 1322 | '--mymodule-extra=baz', 1323 | '--mymodule-expires=2014-10-15', 1324 | '--mymodule-arbitrary-nesting-depth=works', 1325 | '--extramodule-unique'] 1326 | cfg = LayeredConfig(Defaults(self.types), Commandline(cmdline)) 1327 | self._test_config_subsections(cfg) 1328 | self.assertTrue(cfg.implicitboolean) 1329 | self.assertIs(type(cfg.implicitboolean), bool) 1330 | 1331 | def test_typed_novalue(self): 1332 | # this cmdline only sets some of the settings. The test is 1333 | # that the rest should raise AttributeError (not return None, 1334 | # as was the previous behaviour), and that __iter__ should not 1335 | # include them. 1336 | cmdline = ['--processes=4', '--force=False'] 1337 | cfg = LayeredConfig(Defaults(self.types), Commandline(cmdline)) 1338 | self.assertEqual(4, cfg.processes) 1339 | self.assertIsInstance(cfg.processes, int) 1340 | with self.assertRaises(AttributeError): 1341 | cfg.home 1342 | with self.assertRaises(AttributeError): 1343 | cfg.extra 1344 | self.assertEqual(set(['processes', 'force']), 1345 | set(list(cfg))) 1346 | 1347 | def test_typed_override(self): 1348 | # make sure this auto-typing isn't run for bools 1349 | types = {'logfile': True} 1350 | cmdline = ["--logfile=out.log"] 1351 | cfg = LayeredConfig(Defaults(types), Commandline(cmdline)) 1352 | self.assertEqual(cfg.logfile, "out.log") 1353 | 1354 | def test_typed_commandline_cascade(self): 1355 | # the test here is that __getattribute__ must determine that 1356 | # subconfig.force is not typed in itself, and fetch type 1357 | # information from the root of defaults 1358 | 1359 | defaults = {'force': True, 1360 | 'lastdownload': datetime, 1361 | 'mymodule': {}} 1362 | cmdline = ['--mymodule-force=False'] 1363 | cfg = LayeredConfig(Defaults(defaults), Commandline(cmdline), 1364 | cascade=True) 1365 | subconfig = getattr(cfg, 'mymodule') 1366 | self.assertIs(type(subconfig.force), bool) 1367 | self.assertEqual(subconfig.force, False) 1368 | 1369 | # test typed config values that have no actual value. Since 1370 | # they have no value, they should raise AtttributeError 1371 | with self.assertRaises(AttributeError): 1372 | self.assertEqual(cfg.lastdownload, None) 1373 | with self.assertRaises(AttributeError): 1374 | self.assertEqual(subconfig.lastdownload, None) 1375 | 1376 | def test_commandline_implicit_typing(self): 1377 | # The big test here is really the partially-configured 1378 | # ArgumentParser (handles one positional argument but not the 1379 | # optional --force) 1380 | defaults = {'force': False} 1381 | cmdline = ['command', '--force'] 1382 | parser = argparse.ArgumentParser() 1383 | parser.add_argument("positional") 1384 | cmdlinesrc = Commandline(cmdline, parser=parser) 1385 | cfg = LayeredConfig(Defaults(defaults), cmdlinesrc) 1386 | self.assertEqual(cfg.force, True) 1387 | 1388 | # try again with explicit argument 1389 | parser = argparse.ArgumentParser() 1390 | parser.add_argument("positional") 1391 | cmdlinesrc = Commandline(['command', '--force=True'], parser=parser) 1392 | cfg = LayeredConfig(Defaults(defaults), cmdlinesrc) 1393 | self.assertEqual(cfg.force, True) 1394 | 1395 | # once again without the optional typing source 1396 | parser = argparse.ArgumentParser() 1397 | parser.add_argument("positional") 1398 | cmdlinesrc = Commandline(['command', '--force'], parser=parser) 1399 | cfg = LayeredConfig(Defaults({}), cmdlinesrc) 1400 | self.assertEqual(cfg.force, True) 1401 | 1402 | def test_layered_typing_for_none_values_in_lower_priority(self): 1403 | source1 = Defaults({'key': None}) 1404 | source2 = Environment({'KEY': 3}) 1405 | config = LayeredConfig(source1, source2) 1406 | self.assertEquals(config.key, 3) 1407 | 1408 | 1409 | class TestTypingINIFile(TestINIFileHelper, 1410 | LayeredConfigHelperTests, 1411 | unittest.TestCase): 1412 | types = {'home': str, 1413 | 'processes': int, 1414 | 'force': bool, 1415 | 'extra': list, 1416 | 'mymodule': {'force': bool, 1417 | 'extra': list, 1418 | 'expires': date, 1419 | 'lastrun': datetime, 1420 | } 1421 | } 1422 | 1423 | # FIXME: find a neat way to run the tests in 1424 | # test_config_subsections with a LayeredConfig object that uses a 1425 | # Default object for typing 1426 | def test_typed_inifile(self): 1427 | cfg = LayeredConfig(Defaults(self.types), INIFile("complex.ini")) 1428 | self.supported_types = (str, bool, int, list, date, datetime) 1429 | self.supports_nesting = False 1430 | self._test_config_subsections(cfg) 1431 | 1432 | 1433 | class TestLayered(TestINIFileHelper, unittest.TestCase): 1434 | def test_layered(self): 1435 | defaults = {'home': 'someplace'} 1436 | cmdline = ['--home=anotherplace'] 1437 | env = {'MYAPP_HOME': 'yourdata'} 1438 | cfg = LayeredConfig(Defaults(defaults)) 1439 | self.assertEqual(cfg.home, 'someplace') 1440 | cfg = LayeredConfig(Defaults(defaults), INIFile("simple.ini")) 1441 | self.assertEqual(cfg.home, 'mydata') 1442 | cfg = LayeredConfig(Defaults(defaults), INIFile("simple.ini"), 1443 | Environment(env, prefix="MYAPP_")) 1444 | self.assertEqual(cfg.home, 'yourdata') 1445 | cfg = LayeredConfig(Defaults(defaults), INIFile("simple.ini"), 1446 | Environment(env, prefix="MYAPP_"), 1447 | Commandline(cmdline)) 1448 | self.assertEqual(cfg.home, 'anotherplace') 1449 | self.assertEqual(['home', 'processes', 'force', 'extra', 'expires', 1450 | 'lastrun'], list(cfg)) 1451 | 1452 | def test_layered_subsections(self): 1453 | defaults = OrderedDict((('force', False), 1454 | ('home', 'thisdata'), 1455 | ('loglevel', 'INFO'))) 1456 | cmdline = ['--mymodule-home=thatdata', '--mymodule-force'] 1457 | cfg = LayeredConfig(Defaults(defaults), Commandline(cmdline), 1458 | cascade=True) 1459 | self.assertEqual(cfg.mymodule.force, True) 1460 | self.assertEqual(cfg.mymodule.home, 'thatdata') 1461 | self.assertEqual(cfg.mymodule.loglevel, 'INFO') 1462 | 1463 | # second test is more difficult: the lower-priority Defaults 1464 | # source only contains a subsection, while the higher-priority 1465 | # Commandline source contains no such subsection. Our 1466 | # sub-LayeredConfig object will only have a Defaults source, 1467 | # not a Commandline source (which will cause the 1468 | # __getattribute__ lookup_resource to look in the Defaults 1469 | # object in the sub-LayeredConfig object, unless we do 1470 | # something smart. 1471 | defaults = {'mymodule': defaults} 1472 | cmdline = ['--home=thatdata', '--force'] 1473 | 1474 | o = Commandline(cmdline) 1475 | o.subsection("mymodule").keys() 1476 | cfg = LayeredConfig(Defaults(defaults), Commandline(cmdline), 1477 | cascade=True) 1478 | self.assertEqual(cfg.mymodule.force, True) 1479 | self.assertEqual(cfg.mymodule.home, 'thatdata') 1480 | self.assertEqual(cfg.mymodule.loglevel, 'INFO') 1481 | self.assertEqual(['force', 'home', 'loglevel'], list(cfg.mymodule)) 1482 | 1483 | 1484 | class TestSubsections(unittest.TestCase): 1485 | def test_list(self): 1486 | defaults = {'home': 'mydata', 1487 | 'subsection': {'processes': 4}} 1488 | cfg = LayeredConfig(Defaults(defaults), 1489 | cascade=True) 1490 | self.assertEqual(set(['home', 'processes']), 1491 | set(cfg.subsection)) 1492 | 1493 | def test_subsection_respects_subclass(self): 1494 | defaults = { 1495 | 'subsection': { 1496 | 'processes': 4 1497 | } 1498 | } 1499 | 1500 | class SubclassedLayeredConfig(LayeredConfig): 1501 | pass 1502 | 1503 | cfg = SubclassedLayeredConfig(Defaults(defaults)) 1504 | self.assertIsInstance(cfg.subsection, SubclassedLayeredConfig) 1505 | 1506 | def test_cascading_parent_subsections(self): 1507 | defaults = {'home': 'mydata', 1508 | 'subsection': {'processes': 4}} 1509 | cfg = LayeredConfig(Defaults(defaults), 1510 | cascade=True) 1511 | # if the DictSource has a bug in its has() implementatation, 1512 | # and we use cascade, any subsection will have an automatic 1513 | # subsection with the same name of its own, and which will 1514 | # yield a raw dict 1515 | with self.assertRaises(AttributeError): 1516 | cfg.subsection.subsection 1517 | 1518 | 1519 | class TestLayeredSubsections(unittest.TestCase): 1520 | 1521 | def _test_subsection(self, primary, secondary, cls): 1522 | with open("primary.txt", "w") as fp: 1523 | fp.write(primary) 1524 | with open("secondary.txt", "w") as fp: 1525 | fp.write(secondary) 1526 | try: 1527 | srcs = [cls('primary.txt'), cls('secondary.txt')] 1528 | cfg = LayeredConfig(*srcs) 1529 | self.assertEqual(cfg.somevar, 'value') 1530 | self.assertEqual(cfg.a.b, 'b') 1531 | finally: 1532 | os.unlink("primary.txt") 1533 | os.unlink("secondary.txt") 1534 | 1535 | def test_layered_yaml(self): 1536 | self._test_subsection("""a: 1537 | b: b 1538 | """, "somevar: value", YAMLFile) 1539 | 1540 | def test_layered_ini(self): 1541 | self._test_subsection(""" 1542 | [__root__] 1543 | 1544 | [a] 1545 | 1546 | b = b 1547 | """, """ 1548 | [__root__] 1549 | somevar = value 1550 | """, INIFile) 1551 | 1552 | def test_layered_json(self): 1553 | self._test_subsection('{"a": {"b": "b"} }', 1554 | '{"somevar": "value"}', 1555 | JSONFile) 1556 | 1557 | def test_layered_plist(self): 1558 | self._test_subsection(""" 1559 | 1560 | 1561 | 1562 | a 1563 | 1564 | b 1565 | b 1566 | 1567 | 1568 | """, """ 1569 | 1570 | 1571 | 1572 | somevar 1573 | value 1574 | 1575 | """, PListFile) 1576 | 1577 | 1578 | class TestLayeredWithSingleSource(unittest.TestCase): 1579 | # generalization of the issue demonstrated by 1580 | # https://github.com/staffanm/layeredconfig/issues/8 -- sources 1581 | # that cannot be duplicated/layered by themselves (Environment, 1582 | # Commandline, EtcdStore) should be tested with other sources, 1583 | # particularly with subsections 1584 | 1585 | def setUp(self): 1586 | with open("simple.yaml", "w") as fp: 1587 | fp.write(""" 1588 | section: 1589 | subsection: 1590 | key: value 1591 | """) 1592 | self.yamlsource = YAMLFile("simple.yaml") 1593 | 1594 | def tearDown(self): 1595 | os.unlink("simple.yaml") 1596 | 1597 | def test_commandline(self): 1598 | cfg = LayeredConfig(self.yamlsource, Commandline()) 1599 | self.assertEqual("value", cfg.section.subsection.key) 1600 | 1601 | cmdline = ["./foo.py", "--foo=bar"] 1602 | cfg = LayeredConfig(self.yamlsource, Commandline(cmdline)) 1603 | self.assertEqual("value", cfg.section.subsection.key) 1604 | 1605 | cmdline = ["./foo.py", "--foo=bar", "--section-subsection-key=other"] 1606 | cfg = LayeredConfig(self.yamlsource, Commandline(cmdline)) 1607 | self.assertEqual("other", cfg.section.subsection.key) 1608 | 1609 | def test_environment(self): 1610 | cfg = LayeredConfig(self.yamlsource, Environment()) 1611 | self.assertEqual("value", cfg.section.subsection.key) 1612 | 1613 | env = {'MYAPP_FOO': 'bar'} 1614 | cfg = LayeredConfig(self.yamlsource, Environment(env, prefix="MYAPP_")) 1615 | self.assertEqual("value", cfg.section.subsection.key) 1616 | 1617 | env = {'MYAPP_FOO': 'bar', 1618 | 'MYAPP_SECTION_SUBSECTION_KEY': 'other'} 1619 | cfg = LayeredConfig(self.yamlsource, Environment(env, prefix="MYAPP_")) 1620 | self.assertEqual("other", cfg.section.subsection.key) 1621 | 1622 | 1623 | 1624 | class TestModifications(TestINIFileHelper, unittest.TestCase): 1625 | def test_modified(self): 1626 | defaults = {'lastdownload': None} 1627 | cfg = LayeredConfig(Defaults(defaults)) 1628 | now = datetime.now() 1629 | cfg.lastdownload = now 1630 | self.assertEqual(cfg.lastdownload, now) 1631 | 1632 | def test_modified_subsections(self): 1633 | defaults = {'force': False, 1634 | 'home': 'thisdata', 1635 | 'loglevel': 'INFO'} 1636 | cmdline = ['--mymodule-home=thatdata', '--mymodule-force'] 1637 | cfg = LayeredConfig(Defaults(defaults), 1638 | INIFile("complex.ini"), 1639 | Commandline(cmdline), 1640 | cascade=True) 1641 | cfg.mymodule.expires = date(2014, 10, 24) 1642 | 1643 | def test_modified_singlesource_subsection(self): 1644 | self.globalconf = LayeredConfig( 1645 | Defaults({'download_text': None, 1646 | 'base': {}}), 1647 | cascade=True) 1648 | # this should't raise an AttributeError 1649 | self.globalconf.base.download_text 1650 | # this shouldn't, either 1651 | self.globalconf.base.download_text = "WHAT" 1652 | 1653 | def test_write_noconfigfile(self): 1654 | cfg = LayeredConfig(Defaults({'lastrun': 1655 | datetime(2012, 9, 18, 15, 41, 0)})) 1656 | cfg.lastrun = datetime(2013, 9, 18, 15, 41, 0) 1657 | LayeredConfig.write(cfg) 1658 | 1659 | def test_set_novalue(self): 1660 | # it should be possible to set values that are defined in any 1661 | # of the configsources, even though only typing information 1662 | # exists there. 1663 | cfg = LayeredConfig(Defaults({'placeholder': int}), 1664 | Commandline([])) 1665 | cfg.placeholder = 42 1666 | 1667 | # but it shouldn't be possible to set values that hasn't been 1668 | # defined anywhere. 1669 | with self.assertRaises(AttributeError): 1670 | cfg.nonexistent = 43 1671 | 1672 | 1673 | class TestAccessors(TestINIFileHelper, unittest.TestCase): 1674 | 1675 | def test_set(self): 1676 | # a value is set in a particular underlying source, and the 1677 | # dirty flag isn't set. 1678 | cfg = LayeredConfig(INIFile("simple.ini")) 1679 | LayeredConfig.set(cfg, 'expires', date(2013, 9, 18), 1680 | "inifile") 1681 | # NOTE: For this config, where no type information is 1682 | # available for key 'expires', INIFile.set will convert the 1683 | # date object to a string, at which point typing is lost. 1684 | # Therefore this commmented-out test will fail 1685 | # self.assertEqual(date(2013, 9, 18), cfg.expires) 1686 | self.assertEqual("2013-09-18", cfg.expires) 1687 | self.assertFalse(cfg._sources[0].dirty) 1688 | 1689 | def test_get(self): 1690 | cfg = LayeredConfig(Defaults({'codedefaults': 'yes', 1691 | 'force': False, 1692 | 'home': '/usr/home'}), 1693 | INIFile('simple.ini')) 1694 | # and then do a bunch of get() calls with optional fallbacks 1695 | self.assertEqual("yes", LayeredConfig.get(cfg, "codedefaults")) 1696 | self.assertEqual("mydata", LayeredConfig.get(cfg, "home")) 1697 | self.assertEqual(None, LayeredConfig.get(cfg, "nonexistent")) 1698 | self.assertEqual("NO!", LayeredConfig.get(cfg, "nonexistent", "NO!")) 1699 | 1700 | 1701 | class TestDump(unittest.TestCase): 1702 | def test_dump(self): 1703 | defaults = { 1704 | 'home': 'mydata', 1705 | 'processes': 4, 1706 | 'force': True, 1707 | 'extra': ['foo', 'bar'], 1708 | 'mymodule': { 1709 | 'force': False, 1710 | 'extra': ['foo', 'baz'], 1711 | 'arbitrary': { 1712 | 'nesting': { 1713 | 'depth': 'works' 1714 | } 1715 | } 1716 | }, 1717 | 'extramodule': { 1718 | 'unique': True 1719 | } 1720 | } 1721 | 1722 | config = LayeredConfig(Defaults(defaults)) 1723 | self.assertEquals(defaults, LayeredConfig.dump(config)) 1724 | 1725 | 1726 | if __name__ == '__main__': 1727 | unittest.main() 1728 | -------------------------------------------------------------------------------- /tests/test_withFuture.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals 4 | try: 5 | from builtins import * 6 | from future import standard_library 7 | standard_library.install_aliases() 8 | except: 9 | # we might be on py3.2, which the future library doesn't support 10 | pass 11 | 12 | import os 13 | import sys 14 | 15 | if sys.version_info < (2, 7, 0): # pragma: no cover 16 | import unittest2 as unittest 17 | else: 18 | import unittest 19 | 20 | from layeredconfig import LayeredConfig, Defaults, Environment, INIFile 21 | 22 | @unittest.skipIf (sys.version_info[0] == 3 and sys.version_info[1] < 3, 23 | "Python 3.2 and lower doesn't support the future module") 24 | class TestFuture(unittest.TestCase): 25 | 26 | def test_newint(self): 27 | os.environ['FERENDA_DOWNLOADMAX'] = '3' 28 | config = LayeredConfig(Defaults({'downloadmax': int}), 29 | Environment(prefix="FERENDA_")) 30 | self.assertEqual(3, config.downloadmax) 31 | self.assertIsInstance(config.downloadmax, int) 32 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26, py27, py32, py33, py34, py35 3 | 4 | [testenv] 5 | setenv = 6 | PYTHONPATH = {toxinidir}:{toxinidir}/layeredconfig 7 | commands = pytest tests {posargs} 8 | deps = 9 | pytest 10 | --------------------------------------------------------------------------------