├── .coveragerc ├── .github └── workflows │ └── python-test.yml ├── .gitignore ├── CHANGES.rst ├── LICENSE ├── README.md ├── docs ├── Makefile ├── conf.py ├── configobj.rst ├── default.css ├── docutils.conf ├── docutils.css ├── index.rst ├── make.bat ├── pygments.css ├── template.tmp └── validate.rst ├── pyproject.toml ├── setup.cfg ├── setup.py ├── setup_validate.py └── src ├── configobj ├── __init__.py ├── _version.py └── validate.py ├── tests ├── __init__.py ├── conf.ini ├── conf.spec ├── configobj_doctests.py ├── conftest.py ├── test_configobj.py ├── test_validate.py └── test_validate_errors.py └── validate └── __init__.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | -------------------------------------------------------------------------------- /.github/workflows/python-test.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-on-latest: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install pytest coverage pytest-cov 24 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 25 | pip install -e . 26 | - name: Test with pytest 27 | run: | 28 | python src/tests/configobj_doctests.py 29 | python -m configobj.validate 30 | py.test -c setup.cfg --color=yes --cov=configobj --cov-report=term --cov-report=html --cov-report=xml 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | docs/_build 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | __pycache__ 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | .tox 29 | nosetests.xml 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | .idea 39 | 40 | # virtualenv 41 | env 42 | 43 | # autogenerated by distutils 44 | MANIFEST 45 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | --------- 3 | 4 | Release 5.0.9 5 | """"""""""""" 6 | 7 | * drop support for Python 2 and <3.7 8 | * fix CVE-2023-26112, ReDoS attack 9 | 10 | Release 5.0.8 11 | """"""""""""" 12 | 13 | * fixing/test for a regression introduced in 5.0.7 that prevented ``import validate`` from working 14 | 15 | 16 | Release 5.0.7 17 | """"""""""""" 18 | 19 | * update testing to validate against python version 2.7 and 3.5-3.11 20 | * update broken links / non-existent services and references 21 | 22 | Older Releases 23 | """""""""""""" 24 | 25 | * Release 5.0.6 improves error messages in certain edge cases 26 | * Release 5.0.5 corrects a unicode-bug that still existed in writing files 27 | * Release 5.0.4 corrects a unicode-bug that still existed in reading files after 28 | fixing lists of string in 5.0.3 29 | * Release 5.0.3 corrects errors related to the incorrectly handling unicode 30 | encoding and writing out files 31 | * Release 5.0.2 adds a specific error message when trying to install on 32 | Python versions older than 2.5 33 | * Release 5.0.1 fixes a regression with unicode conversion not happening 34 | in certain cases PY2 35 | * Release 5.0.0 updates the supported Python versions to 2.6, 2.7, 3.2, 3.3 36 | and is otherwise unchanged 37 | * Release 4.7.2 fixes several bugs in 4.7.1 38 | * Release 4.7.1 fixes a bug with the deprecated options keyword in 4.7.0. 39 | * Release 4.7.0 improves performance adds features for validation and 40 | fixes some bugs. 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c): 2 | 2003-2010, Michael Foord, Nicola Larosa 3 | 2014-2023, Eli Courtwright, Rob Dennis 4 | All rights reserved. 5 | E-mails : 6 | michael AT python DOT org 7 | nico AT tekNico DOT net 8 | eli AT courtwright DOT org 9 | rdennis AT gmail DOT com 10 | 11 | 12 | Redistribution and use in source and binary forms, with or without 13 | modification, are permitted provided that the following conditions are 14 | met: 15 | 16 | * Redistributions of source code must retain the above copyright 17 | notice, this list of conditions and the following disclaimer. 18 | 19 | * Redistributions in binary form must reproduce the above 20 | copyright notice, this list of conditions and the following 21 | disclaimer in the documentation and/or other materials provided 22 | with the distribution. 23 | 24 | * Neither the names of Michael Foord, Eli Courtwright or Rob Dennis, 25 | nor the name of Voidspace, may be used to endorse or promote 26 | products derived from this software without specific prior written 27 | permission. 28 | 29 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 30 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 31 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 32 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 33 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 34 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 35 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 36 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 37 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 38 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 39 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # configobj 2 | [![Coverage Status](https://img.shields.io/coveralls/DiffSK/configobj.svg)](https://coveralls.io/r/DiffSK/configobj?branch=master) 3 | [![PyPI version](http://img.shields.io/pypi/v/configobj.svg)](https://pypi.python.org/pypi/configobj) 4 | [![License](https://img.shields.io/badge/license-BSD_3--clause-red.svg)](https://github.com/DiffSK/configobj/blob/master/LICENSE) 5 | 6 | 7 | Python 3+ compatible port of the [configobj](https://pypi.python.org/pypi/configobj/) library. 8 | 9 | The Github CI/CD Pipeline runs tests on python versions: 10 | - 3.8 11 | - 3.9 12 | - 3.10 13 | - 3.11 14 | - 3.12 15 | - 3.13 16 | 17 | 18 | ## Documentation 19 | 20 | You can find a full manual on how to use ConfigObj at [readthedocs](http://configobj.readthedocs.io/). 21 | 22 | ## Status 23 | 24 | This is a mature project that is not actively maintained at this time. 25 | 26 | ## Past Contributors: 27 | 28 | - [Michael Foord](https://agileabstractions.com/) 29 | - original creator of ``configobj`` and ``validate`` and maintainer through version 4 30 | - [Rob Dennis](https://github.com/robdennis) 31 | - released version 5 (first python 3-compatible release) in 2014, bringing the project to github 32 | - released the last maintenance release (until new maintainership is established) in 2023 33 | - [Eli Courtwright](https://github.com/EliAndrewC) 34 | - released version 5 (first python 3-compatible release) in 2014 35 | - [Nicola Larosa](https://pypi.org/user/tekNico/) 36 | - Contributions to the pre-version 5 codebase 37 | - [Jürgen Hermann](https://github.com/jhermann) 38 | - day-to-day maintenance of the repo 39 | -------------------------------------------------------------------------------- /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/configobj.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/configobj.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/configobj" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/configobj" 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." 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # configobj documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Feb 8 01:26:54 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | #sys.path.insert(0, os.path.abspath('.')) 19 | 20 | # -- General configuration ------------------------------------------------ 21 | 22 | # If your documentation needs a minimal Sphinx version, state it here. 23 | #needs_sphinx = '1.0' 24 | 25 | # Add any Sphinx extension module names here, as strings. They can be 26 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 27 | # ones. 28 | extensions = [ 29 | 'sphinx.ext.coverage', 30 | ] 31 | 32 | # Add any paths that contain templates here, relative to this directory. 33 | templates_path = ['_templates'] 34 | 35 | # The suffix of source filenames. 36 | source_suffix = '.rst' 37 | 38 | # The encoding of source files. 39 | #source_encoding = 'utf-8-sig' 40 | 41 | # The master toctree document. 42 | master_doc = 'index' 43 | 44 | # General information about the project. 45 | project = u'configobj' 46 | copyright = u'2014, Michael Foord, Nicola Larosa, Rob Dennis, Eli Courtwright' 47 | 48 | # The version info for the project you're documenting, acts as replacement for 49 | # |version| and |release|, also used in various other places throughout the 50 | # built documents. 51 | # 52 | # The full version, including alpha/beta/rc tags. 53 | release = '5.0.6' 54 | # The short X.Y version. 55 | version = '.'.join(release.split('.')[:2]) 56 | 57 | # The language for content autogenerated by Sphinx. Refer to documentation 58 | # for a list of supported languages. 59 | #language = None 60 | 61 | # There are two options for replacing |today|: either, you set today to some 62 | # non-false value, then it is used: 63 | #today = '' 64 | # Else, today_fmt is used as the format for a strftime call. 65 | #today_fmt = '%B %d, %Y' 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | exclude_patterns = ['_build'] 70 | 71 | # The reST default role (used for this markup: `text`) to use for all 72 | # documents. 73 | #default_role = None 74 | 75 | # If true, '()' will be appended to :func: etc. cross-reference text. 76 | #add_function_parentheses = True 77 | 78 | # If true, the current module name will be prepended to all description 79 | # unit titles (such as .. function::). 80 | #add_module_names = True 81 | 82 | # If true, sectionauthor and moduleauthor directives will be shown in the 83 | # output. They are ignored by default. 84 | #show_authors = False 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | pygments_style = 'sphinx' 88 | 89 | # A list of ignored prefixes for module index sorting. 90 | #modindex_common_prefix = [] 91 | 92 | # If true, keep warnings as "system message" paragraphs in the built documents. 93 | #keep_warnings = False 94 | 95 | 96 | # -- Options for HTML output ---------------------------------------------- 97 | 98 | # The theme to use for HTML and HTML Help pages. See the documentation for 99 | # a list of builtin themes. 100 | html_theme = 'default' 101 | 102 | # Theme options are theme-specific and customize the look and feel of a theme 103 | # further. For a list of options available for each theme, see the 104 | # documentation. 105 | #html_theme_options = {} 106 | 107 | # Add any paths that contain custom themes here, relative to this directory. 108 | #html_theme_path = [] 109 | 110 | # The name for this set of Sphinx documents. If None, it defaults to 111 | # " v documentation". 112 | #html_title = None 113 | 114 | # A shorter title for the navigation bar. Default is the same as html_title. 115 | #html_short_title = None 116 | 117 | # The name of an image file (relative to this directory) to place at the top 118 | # of the sidebar. 119 | #html_logo = None 120 | 121 | # The name of an image file (within the static path) to use as favicon of the 122 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 123 | # pixels large. 124 | #html_favicon = None 125 | 126 | # Add any paths that contain custom static files (such as style sheets) here, 127 | # relative to this directory. They are copied after the builtin static files, 128 | # so a file named "default.css" will overwrite the builtin "default.css". 129 | html_static_path = ['_static'] 130 | 131 | # Add any extra paths that contain custom files (such as robots.txt or 132 | # .htaccess) here, relative to this directory. These files are copied 133 | # directly to the root of the documentation. 134 | #html_extra_path = [] 135 | 136 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 137 | # using the given strftime format. 138 | #html_last_updated_fmt = '%b %d, %Y' 139 | 140 | # If true, SmartyPants will be used to convert quotes and dashes to 141 | # typographically correct entities. 142 | #html_use_smartypants = True 143 | 144 | # Custom sidebar templates, maps document names to template names. 145 | #html_sidebars = {} 146 | 147 | # Additional templates that should be rendered to pages, maps page names to 148 | # template names. 149 | #html_additional_pages = {} 150 | 151 | # If false, no module index is generated. 152 | #html_domain_indices = True 153 | 154 | # If false, no index is generated. 155 | #html_use_index = True 156 | 157 | # If true, the index is split into individual pages for each letter. 158 | #html_split_index = False 159 | 160 | # If true, links to the reST sources are added to the pages. 161 | #html_show_sourcelink = True 162 | 163 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 164 | #html_show_sphinx = True 165 | 166 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 167 | #html_show_copyright = True 168 | 169 | # If true, an OpenSearch description file will be output, and all pages will 170 | # contain a tag referring to it. The value of this option must be the 171 | # base URL from which the finished HTML is served. 172 | #html_use_opensearch = '' 173 | 174 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 175 | #html_file_suffix = None 176 | 177 | # Output file base name for HTML help builder. 178 | htmlhelp_basename = 'configobjdoc' 179 | 180 | 181 | # -- Options for LaTeX output --------------------------------------------- 182 | 183 | latex_elements = { 184 | # The paper size ('letterpaper' or 'a4paper'). 185 | #'papersize': 'letterpaper', 186 | 187 | # The font size ('10pt', '11pt' or '12pt'). 188 | #'pointsize': '10pt', 189 | 190 | # Additional stuff for the LaTeX preamble. 191 | #'preamble': '', 192 | } 193 | 194 | # Grouping the document tree into LaTeX files. List of tuples 195 | # (source start file, target name, title, 196 | # author, documentclass [howto, manual, or own class]). 197 | latex_documents = [ 198 | ('index', 'configobj.tex', u'configobj Documentation', 199 | u'Michael Foord, Nicola Larosa, Rob Dennis, Eli Courtwright', 'manual'), 200 | ] 201 | 202 | # The name of an image file (relative to this directory) to place at the top of 203 | # the title page. 204 | #latex_logo = None 205 | 206 | # For "manual" documents, if this is true, then toplevel headings are parts, 207 | # not chapters. 208 | #latex_use_parts = False 209 | 210 | # If true, show page references after internal links. 211 | #latex_show_pagerefs = False 212 | 213 | # If true, show URL addresses after external links. 214 | #latex_show_urls = False 215 | 216 | # Documents to append as an appendix to all manuals. 217 | #latex_appendices = [] 218 | 219 | # If false, no module index is generated. 220 | #latex_domain_indices = True 221 | 222 | 223 | # -- Options for manual page output --------------------------------------- 224 | 225 | # One entry per manual page. List of tuples 226 | # (source start file, name, description, authors, manual section). 227 | man_pages = [ 228 | ('index', 'configobj', u'configobj Documentation', 229 | [u'Michael Foord, Nicola Larosa, Rob Dennis, Eli Courtwright'], 1) 230 | ] 231 | 232 | # If true, show URL addresses after external links. 233 | #man_show_urls = False 234 | 235 | 236 | # -- Options for Texinfo output ------------------------------------------- 237 | 238 | # Grouping the document tree into Texinfo files. List of tuples 239 | # (source start file, target name, title, author, 240 | # dir menu entry, description, category) 241 | texinfo_documents = [ 242 | ('index', 'configobj', u'configobj Documentation', 243 | u'Michael Foord, Nicola Larosa, Rob Dennis, Eli Courtwright', 'configobj', 'One line description of project.', 244 | 'Miscellaneous'), 245 | ] 246 | 247 | # Documents to append as an appendix to all manuals. 248 | #texinfo_appendices = [] 249 | 250 | # If false, no module index is generated. 251 | #texinfo_domain_indices = True 252 | 253 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 254 | #texinfo_show_urls = 'footnote' 255 | 256 | # If true, do not generate a @detailmenu in the "Top" node's menu. 257 | #texinfo_no_detailmenu = False 258 | -------------------------------------------------------------------------------- /docs/default.css: -------------------------------------------------------------------------------- 1 | /* 2 | :Author: David Goodger (goodger@python.org) 3 | :Id: $Id: html4css1.css 5951 2009-05-18 18:03:10Z milde $ 4 | :Copyright: This stylesheet has been placed in the public domain. 5 | 6 | Default cascading style sheet for the HTML output of Docutils. 7 | 8 | See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to 9 | customize this style sheet. 10 | */ 11 | 12 | /* used to remove borders from tables and images */ 13 | .borderless, table.borderless td, table.borderless th { 14 | border: 0 } 15 | 16 | table.borderless td, table.borderless th { 17 | /* Override padding for "table.docutils td" with "! important". 18 | The right padding separates the table cells. */ 19 | padding: 0 0.5em 0 0 ! important } 20 | 21 | .first { 22 | /* Override more specific margin styles with "! important". */ 23 | margin-top: 0 ! important } 24 | 25 | .last, .with-subtitle { 26 | margin-bottom: 0 ! important } 27 | 28 | .hidden { 29 | display: none } 30 | 31 | a.toc-backref { 32 | text-decoration: none ; 33 | color: black } 34 | 35 | blockquote.epigraph { 36 | margin: 2em 5em ; } 37 | 38 | dl.docutils dd { 39 | margin-bottom: 0.5em } 40 | 41 | /* Uncomment (and remove this text!) to get bold-faced definition list terms 42 | dl.docutils dt { 43 | font-weight: bold } 44 | */ 45 | 46 | div.abstract { 47 | margin: 2em 5em } 48 | 49 | div.abstract p.topic-title { 50 | font-weight: bold ; 51 | text-align: center } 52 | 53 | div.admonition, div.attention, div.caution, div.danger, div.error, 54 | div.hint, div.important, div.note, div.tip, div.warning { 55 | margin: 2em ; 56 | border: medium outset ; 57 | padding: 1em } 58 | 59 | div.admonition p.admonition-title, div.hint p.admonition-title, 60 | div.important p.admonition-title, div.note p.admonition-title, 61 | div.tip p.admonition-title { 62 | font-weight: bold ; 63 | font-family: sans-serif } 64 | 65 | div.attention p.admonition-title, div.caution p.admonition-title, 66 | div.danger p.admonition-title, div.error p.admonition-title, 67 | div.warning p.admonition-title { 68 | color: red ; 69 | font-weight: bold ; 70 | font-family: sans-serif } 71 | 72 | /* Uncomment (and remove this text!) to get reduced vertical space in 73 | compound paragraphs. 74 | div.compound .compound-first, div.compound .compound-middle { 75 | margin-bottom: 0.5em } 76 | 77 | div.compound .compound-last, div.compound .compound-middle { 78 | margin-top: 0.5em } 79 | */ 80 | 81 | div.dedication { 82 | margin: 2em 5em ; 83 | text-align: center ; 84 | font-style: italic } 85 | 86 | div.dedication p.topic-title { 87 | font-weight: bold ; 88 | font-style: normal } 89 | 90 | div.figure { 91 | margin-left: 2em ; 92 | margin-right: 2em } 93 | 94 | div.footer, div.header { 95 | clear: both; 96 | font-size: smaller } 97 | 98 | div.line-block { 99 | display: block ; 100 | margin-top: 1em ; 101 | margin-bottom: 1em } 102 | 103 | div.line-block div.line-block { 104 | margin-top: 0 ; 105 | margin-bottom: 0 ; 106 | margin-left: 1.5em } 107 | 108 | div.sidebar { 109 | margin: 0 0 0.5em 1em ; 110 | border: medium outset ; 111 | padding: 1em ; 112 | background-color: #ffffee ; 113 | width: 40% ; 114 | float: right ; 115 | clear: right } 116 | 117 | div.sidebar p.rubric { 118 | font-family: sans-serif ; 119 | font-size: medium } 120 | 121 | div.system-messages { 122 | margin: 5em } 123 | 124 | div.system-messages h1 { 125 | color: red } 126 | 127 | div.system-message { 128 | border: medium outset ; 129 | padding: 1em } 130 | 131 | div.system-message p.system-message-title { 132 | color: red ; 133 | font-weight: bold } 134 | 135 | div.topic { 136 | margin: 2em } 137 | 138 | h1.section-subtitle, h2.section-subtitle, h3.section-subtitle, 139 | h4.section-subtitle, h5.section-subtitle, h6.section-subtitle { 140 | margin-top: 0.4em } 141 | 142 | h1.title { 143 | text-align: center } 144 | 145 | h2.subtitle { 146 | text-align: center } 147 | 148 | hr.docutils { 149 | width: 75% } 150 | 151 | img.align-left, .figure.align-left{ 152 | clear: left ; 153 | float: left ; 154 | margin-right: 1em } 155 | 156 | img.align-right, .figure.align-right { 157 | clear: right ; 158 | float: right ; 159 | margin-left: 1em } 160 | 161 | .align-left { 162 | text-align: left } 163 | 164 | .align-center { 165 | clear: both ; 166 | text-align: center } 167 | 168 | .align-right { 169 | text-align: right } 170 | 171 | /* reset inner alignment in figures */ 172 | div.align-right { 173 | text-align: left } 174 | 175 | /* div.align-center * { */ 176 | /* text-align: left } */ 177 | 178 | ol.simple, ul.simple { 179 | margin-bottom: 1em } 180 | 181 | ol.arabic { 182 | list-style: decimal } 183 | 184 | ol.loweralpha { 185 | list-style: lower-alpha } 186 | 187 | ol.upperalpha { 188 | list-style: upper-alpha } 189 | 190 | ol.lowerroman { 191 | list-style: lower-roman } 192 | 193 | ol.upperroman { 194 | list-style: upper-roman } 195 | 196 | p.attribution { 197 | text-align: right ; 198 | margin-left: 50% } 199 | 200 | p.caption { 201 | font-style: italic } 202 | 203 | p.credits { 204 | font-style: italic ; 205 | font-size: smaller } 206 | 207 | p.label { 208 | white-space: nowrap } 209 | 210 | p.rubric { 211 | font-weight: bold ; 212 | font-size: larger ; 213 | color: maroon ; 214 | text-align: center } 215 | 216 | p.sidebar-title { 217 | font-family: sans-serif ; 218 | font-weight: bold ; 219 | font-size: larger } 220 | 221 | p.sidebar-subtitle { 222 | font-family: sans-serif ; 223 | font-weight: bold } 224 | 225 | p.topic-title { 226 | font-weight: bold } 227 | 228 | pre.address { 229 | margin-bottom: 0 ; 230 | margin-top: 0 ; 231 | font: inherit } 232 | 233 | pre.literal-block, pre.doctest-block { 234 | margin-left: 2em ; 235 | margin-right: 2em } 236 | 237 | span.classifier { 238 | font-family: sans-serif ; 239 | font-style: oblique } 240 | 241 | span.classifier-delimiter { 242 | font-family: sans-serif ; 243 | font-weight: bold } 244 | 245 | span.interpreted { 246 | font-family: sans-serif } 247 | 248 | span.option { 249 | white-space: nowrap } 250 | 251 | span.pre { 252 | white-space: pre } 253 | 254 | span.problematic { 255 | color: red } 256 | 257 | span.section-subtitle { 258 | /* font-size relative to parent (h1..h6 element) */ 259 | font-size: 80% } 260 | 261 | table.citation { 262 | border-left: solid 1px gray; 263 | margin-left: 1px } 264 | 265 | table.docinfo { 266 | margin: 2em 4em } 267 | 268 | table.docutils { 269 | margin-top: 0.5em ; 270 | margin-bottom: 0.5em } 271 | 272 | table.footnote { 273 | border-left: solid 1px black; 274 | margin-left: 1px } 275 | 276 | table.docutils td, table.docutils th, 277 | table.docinfo td, table.docinfo th { 278 | padding-left: 0.5em ; 279 | padding-right: 0.5em ; 280 | vertical-align: top } 281 | 282 | table.docutils th.field-name, table.docinfo th.docinfo-name { 283 | font-weight: bold ; 284 | text-align: left ; 285 | white-space: nowrap ; 286 | padding-left: 0 } 287 | 288 | h1 tt.docutils, h2 tt.docutils, h3 tt.docutils, 289 | h4 tt.docutils, h5 tt.docutils, h6 tt.docutils { 290 | font-size: 100% } 291 | 292 | ul.auto-toc { 293 | list-style-type: none } 294 | -------------------------------------------------------------------------------- /docs/docutils.conf: -------------------------------------------------------------------------------- 1 | [html4css1 writer] 2 | embed-stylesheet: no 3 | cloak_email_addresses: yes 4 | stylesheet: docutils.css 5 | template: template.tmp -------------------------------------------------------------------------------- /docs/docutils.css: -------------------------------------------------------------------------------- 1 | /* 2 | :Authors: Ian Bicking, Michael Foord 3 | :Contact: fuzzyman@voidspace.org.uk 4 | :Date: 2005/08/26 5 | :Version: 0.1.0 6 | :Copyright: This stylesheet has been placed in the public domain. 7 | 8 | Stylesheet for Docutils. 9 | Based on ``blue_box.css`` by Ian Bicking 10 | and ``default.css`` revision 3442 11 | */ 12 | 13 | @import url(default.css); 14 | @import url(pygments.css); 15 | 16 | body { 17 | font-family: Arial, sans-serif; 18 | } 19 | 20 | em, i { 21 | /* Typically serif fonts have much nicer italics */ 22 | font-family: Times New Roman, Times, serif; 23 | } 24 | 25 | 26 | a.target { 27 | color: blue; 28 | } 29 | 30 | a.toc-backref { 31 | text-decoration: none; 32 | color: black; 33 | } 34 | 35 | a.toc-backref:hover { 36 | background-color: inherit; 37 | } 38 | 39 | a:hover { 40 | background-color: #cccccc; 41 | } 42 | 43 | div.attention, div.caution, div.danger, div.error, div.hint, 44 | div.important, div.note, div.tip, div.warning { 45 | background-color: #cccccc; 46 | padding: 3px; 47 | width: 80%; 48 | } 49 | 50 | div.admonition p.admonition-title, div.hint p.admonition-title, 51 | div.important p.admonition-title, div.note p.admonition-title, 52 | div.tip p.admonition-title { 53 | text-align: center; 54 | background-color: #999999; 55 | display: block; 56 | margin: 0; 57 | } 58 | 59 | div.attention p.admonition-title, div.caution p.admonition-title, 60 | div.danger p.admonition-title, div.error p.admonition-title, 61 | div.warning p.admonition-title { 62 | color: #cc0000; 63 | font-family: sans-serif; 64 | text-align: center; 65 | background-color: #999999; 66 | display: block; 67 | margin: 0; 68 | } 69 | 70 | h1, h2, h3, h4, h5, h6 { 71 | font-family: Helvetica, Arial, sans-serif; 72 | border: thin solid black; 73 | /* This makes the borders rounded on Mozilla, which pleases me */ 74 | -moz-border-radius: 8px; 75 | padding: 4px; 76 | } 77 | 78 | h1 { 79 | background-color: #444499; 80 | color: #ffffff; 81 | border: medium solid black; 82 | } 83 | 84 | h1 a.toc-backref, h2 a.toc-backref { 85 | color: #ffffff; 86 | } 87 | 88 | h2 { 89 | background-color: #666666; 90 | color: #ffffff; 91 | border: medium solid black; 92 | } 93 | 94 | h3, h4, h5, h6 { 95 | background-color: #cccccc; 96 | color: #000000; 97 | } 98 | 99 | h3 a.toc-backref, h4 a.toc-backref, h5 a.toc-backref, 100 | h6 a.toc-backref { 101 | color: #000000; 102 | } 103 | 104 | h1.title { 105 | text-align: center; 106 | background-color: #444499; 107 | color: #eeeeee; 108 | border: thick solid black; 109 | -moz-border-radius: 20px; 110 | } 111 | 112 | table.footnote { 113 | padding-left: 0.5ex; 114 | } 115 | 116 | table.citation { 117 | padding-left: 0.5ex 118 | } 119 | 120 | pre.literal-block, pre.doctest-block { 121 | border: thin black solid; 122 | padding: 5px; 123 | } 124 | 125 | .image img { border-style : solid; 126 | border-width : 2px; 127 | } 128 | 129 | img.align-center { 130 | display: block; 131 | margin: auto; 132 | text-align: center; 133 | } 134 | 135 | h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { 136 | font-size: 100%; 137 | } 138 | 139 | code, tt { 140 | color: #000066; 141 | } 142 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. configobj documentation master file, created by 2 | sphinx-quickstart on Sat Feb 8 01:26:54 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to configobj's documentation! 7 | ===================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :numbered: 13 | :maxdepth: 1 14 | 15 | configobj 16 | validate 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | 25 | -------------------------------------------------------------------------------- /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\configobj.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\configobj.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 243 | -------------------------------------------------------------------------------- /docs/pygments.css: -------------------------------------------------------------------------------- 1 | .hll { background-color: #ffffcc } 2 | .c { color: #408080; font-style: italic } /* Comment */ 3 | .err { border: 1px solid #FF0000 } /* Error */ 4 | .k { color: #008000; font-weight: bold } /* Keyword */ 5 | .o { color: #666666 } /* Operator */ 6 | .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 7 | .cp { color: #BC7A00 } /* Comment.Preproc */ 8 | .c1 { color: #408080; font-style: italic } /* Comment.Single */ 9 | .cs { color: #408080; font-style: italic } /* Comment.Special */ 10 | .gd { color: #A00000 } /* Generic.Deleted */ 11 | .ge { font-style: italic } /* Generic.Emph */ 12 | .gr { color: #FF0000 } /* Generic.Error */ 13 | .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 14 | .gi { color: #00A000 } /* Generic.Inserted */ 15 | .go { color: #808080 } /* Generic.Output */ 16 | .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 17 | .gs { font-weight: bold } /* Generic.Strong */ 18 | .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 19 | .gt { color: #0040D0 } /* Generic.Traceback */ 20 | .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ 21 | .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ 22 | .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ 23 | .kp { color: #008000 } /* Keyword.Pseudo */ 24 | .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ 25 | .kt { color: #B00040 } /* Keyword.Type */ 26 | .m { color: #666666 } /* Literal.Number */ 27 | .s { color: #BA2121 } /* Literal.String */ 28 | .na { color: #7D9029 } /* Name.Attribute */ 29 | .nb { color: #008000 } /* Name.Builtin */ 30 | .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 31 | .no { color: #880000 } /* Name.Constant */ 32 | .nd { color: #AA22FF } /* Name.Decorator */ 33 | .ni { color: #999999; font-weight: bold } /* Name.Entity */ 34 | .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 35 | .nf { color: #0000FF } /* Name.Function */ 36 | .nl { color: #A0A000 } /* Name.Label */ 37 | .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 38 | .nt { color: #008000; font-weight: bold } /* Name.Tag */ 39 | .nv { color: #19177C } /* Name.Variable */ 40 | .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 41 | .w { color: #bbbbbb } /* Text.Whitespace */ 42 | .mf { color: #666666 } /* Literal.Number.Float */ 43 | .mh { color: #666666 } /* Literal.Number.Hex */ 44 | .mi { color: #666666 } /* Literal.Number.Integer */ 45 | .mo { color: #666666 } /* Literal.Number.Oct */ 46 | .sb { color: #BA2121 } /* Literal.String.Backtick */ 47 | .sc { color: #BA2121 } /* Literal.String.Char */ 48 | .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ 49 | .s2 { color: #BA2121 } /* Literal.String.Double */ 50 | .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 51 | .sh { color: #BA2121 } /* Literal.String.Heredoc */ 52 | .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 53 | .sx { color: #008000 } /* Literal.String.Other */ 54 | .sr { color: #BB6688 } /* Literal.String.Regex */ 55 | .s1 { color: #BA2121 } /* Literal.String.Single */ 56 | .ss { color: #19177C } /* Literal.String.Symbol */ 57 | .bp { color: #008000 } /* Name.Builtin.Pseudo */ 58 | .vc { color: #19177C } /* Name.Variable.Class */ 59 | .vg { color: #19177C } /* Name.Variable.Global */ 60 | .vi { color: #19177C } /* Name.Variable.Instance */ 61 | .il { color: #666666 } /* Literal.Number.Integer.Long */ 62 | -------------------------------------------------------------------------------- /docs/template.tmp: -------------------------------------------------------------------------------- 1 | %(head_prefix)s 2 | %(head)s 3 | %(stylesheet)s 4 | %(body_prefix)s 5 | %(body_pre_docinfo)s 6 | %(docinfo)s 7 | %(body)s 8 | %(body_suffix)s 9 | -------------------------------------------------------------------------------- /docs/validate.rst: -------------------------------------------------------------------------------- 1 | .. _validate_doc: 2 | 3 | =================================== 4 | Validation Schema with validate.py 5 | =================================== 6 | -------------------------- 7 | Using the Validator class 8 | -------------------------- 9 | 10 | 11 | :Authors: Michael Foord, Nicola Larosa, Rob Dennis, Eli Courtwright, Mark Andrews 12 | :Version: Validate 2.0.0 13 | :Date: 2014/02/08 14 | :Homepage: `Github Page`_ 15 | :License: `BSD License`_ 16 | :Support: `Mailing List`_ 17 | 18 | .. _Mailing List: http://lists.sourceforge.net/lists/listinfo/configobj-develop 19 | .. _This Document: 20 | .. _Github Page: https://github.com/DiffSK/configobj 21 | .. _BSD License: http://opensource.org/licenses/BSD-3-Clause 22 | 23 | 24 | .. contents:: Validate Manual 25 | 26 | 27 | Introduction 28 | ============ 29 | 30 | Validation is used to check that supplied values conform to a specification. 31 | 32 | The value can be supplied as a string, e.g. from a config file. In this case 33 | the check will also *convert* the value to the required type. This allows you 34 | to add validation as a transparent layer to access data stored as strings. The 35 | validation checks that the data is correct *and* converts it to the expected 36 | type. 37 | 38 | Checks are also strings, and are easy to write. One generic system can be used 39 | to validate information from different sources via a single consistent 40 | mechanism. 41 | 42 | Checks look like function calls, and map to function calls. They can include 43 | parameters and keyword arguments. These arguments are passed to the relevant 44 | function by the ``Validator`` instance, along with the value being checked. 45 | 46 | The syntax for checks also allows for specifying a default value. This default 47 | value can be ``None``, no matter what the type of the check. This can be used 48 | to indicate that a value was missing, and so holds no useful value. 49 | 50 | Functions either return a new value, or raise an exception. See `Validator 51 | Exceptions`_ for the low down on the exception classes that ``validate.py`` 52 | defines. 53 | 54 | Some standard functions are provided, for basic data types; these come built 55 | into every validator. Additional checks are easy to write: they can be provided 56 | when the ``Validator`` is instantiated, or added afterwards. 57 | 58 | Validate was primarily written to support ConfigObj, but is designed to be 59 | applicable to many other situations. 60 | 61 | For support and bug reports please use the ConfigObj `Github Page`_ 62 | 63 | 64 | Downloading 65 | =========== 66 | 67 | The current version is **2.0.0**, dated 8th February 2014. 68 | 69 | You can obtain validate in the following ways : 70 | 71 | 72 | Files 73 | ----- 74 | 75 | * validate.py from `Github Page`_ 76 | * The latest development version can be obtained from the `Github Page`_. 77 | 78 | The standard functions 79 | ====================== 80 | 81 | The standard functions come built-in to every ``Validator`` instance. They work 82 | with the following basic data types : 83 | 84 | * integer 85 | * float 86 | * boolean 87 | * string 88 | * ip_addr 89 | 90 | plus lists of these datatypes. 91 | 92 | Adding additional checks is done through coding simple functions. 93 | 94 | The full set of standard checks are : 95 | 96 | :'integer': matches integer values (including negative). Takes optional 'min' 97 | and 'max' arguments:: 98 | 99 | integer() 100 | integer(3, 9) # any value from 3 to 9 101 | integer(min=0) # any positive value 102 | integer(max=9) 103 | 104 | :'float': matches float values 105 | Has the same parameters as the integer check. 106 | 107 | :'boolean': matches boolean values: ``True`` or ``False``. 108 | Acceptable string values for True are:: 109 | 110 | true, on, yes, 1 111 | 112 | Acceptable string values for False are:: 113 | 114 | false, off, no, 0 115 | 116 | Any other value raises an error. 117 | 118 | :'string': matches any string. Takes optional keyword args 'min' and 'max' to 119 | specify min and max length of string. 120 | 121 | :'ip_addr': matches an Internet Protocol address, v.4, represented by a 122 | dotted-quad string, i.e. '1.2.3.4'. 123 | 124 | :'list': matches any list. Takes optional keyword args 'min', and 'max' to 125 | specify min and max sizes of the list. The list checks always 126 | return a list. 127 | 128 | :force_list: matches any list, but if a single value is passed in will 129 | coerce it into a list containing that value. Useful for 130 | configobj if the user forgot the trailing comma to turn 131 | a single value into a list. 132 | 133 | :'tuple': matches any list. This check returns a tuple rather than a list. 134 | 135 | :'int_list': Matches a list of integers. Takes the same arguments as list. 136 | 137 | :'float_list': Matches a list of floats. Takes the same arguments as list. 138 | 139 | :'bool_list': Matches a list of boolean values. Takes the same arguments as 140 | list. 141 | 142 | :'string_list': Matches a list of strings. Takes the same arguments as list. 143 | 144 | :'ip_addr_list': Matches a list of IP addresses. Takes the same arguments as 145 | list. 146 | 147 | :'mixed_list': Matches a list with different types in specific positions. 148 | List size must match the number of arguments. 149 | 150 | Each position can be one of:: 151 | 152 | int, str, boolean, float, ip_addr 153 | 154 | So to specify a list with two strings followed by two integers, 155 | you write the check as:: 156 | 157 | mixed_list(str, str, int, int) 158 | 159 | :'pass': matches everything: it never fails and the value is unchanged. It is 160 | also the default if no check is specified. 161 | 162 | :'option': matches any from a list of options. 163 | You specify this test with:: 164 | 165 | option('option 1', 'option 2', 'option 3') 166 | 167 | The following code will work without you having to specifically add the 168 | functions yourself. 169 | 170 | .. code-block:: python 171 | 172 | from validate import Validator 173 | # 174 | vtor = Validator() 175 | newval1 = vtor.check('integer', value1) 176 | newval2 = vtor.check('boolean', value2) 177 | # etc ... 178 | 179 | .. note:: 180 | 181 | Of course, if these checks fail they raise exceptions. So you should wrap 182 | them in ``try...except`` blocks. Better still, use ConfigObj for a higher 183 | level interface. 184 | 185 | 186 | Using Validator 187 | =============== 188 | 189 | Using ``Validator`` is very easy. It has one public attribute and one public 190 | method. 191 | 192 | Shown below are the different steps in using ``Validator``. 193 | 194 | The only additional thing you need to know, is about `Writing check 195 | functions`_. 196 | 197 | Instantiate 198 | ----------- 199 | 200 | .. code-block:: python 201 | 202 | from validate import Validator 203 | vtor = Validator() 204 | 205 | or even : 206 | 207 | .. code-block:: python 208 | 209 | from validate import Validator 210 | # 211 | fdict = { 212 | 'check_name1': function1, 213 | 'check_name2': function2, 214 | 'check_name3': function3, 215 | } 216 | # 217 | vtor = Validator(fdict) 218 | 219 | 220 | The second method adds a set of your functions as soon as your validator is 221 | created. They are stored in the ``vtor.functions`` dictionary. The 'key' you 222 | give them in this dictionary is the name you use in your checks (not the 223 | original function name). 224 | 225 | Dictionary keys/functions you pass in can override the built-in ones if you 226 | want. 227 | 228 | 229 | Adding functions 230 | ---------------- 231 | 232 | The code shown above, for adding functions on instantiation, has exactly the 233 | same effect as the following code : 234 | 235 | .. code-block:: python 236 | 237 | from validate import Validator 238 | # 239 | vtor = Validator() 240 | vtor.functions['check_name1'] = function1 241 | vtor.functions['check_name2'] = function2 242 | vtor.functions['check_name3'] = function3 243 | 244 | ``vtor.functions`` is just a dictionary that maps names to functions, so we 245 | could also have called ``vtor.functions.update(fdict)``. 246 | 247 | 248 | Writing the check 249 | ----------------- 250 | 251 | As we've heard, the checks map to the names in the ``functions`` dictionary. 252 | You've got a full list of `The standard functions`_ and the arguments they 253 | take. 254 | 255 | If you're using ``Validator`` from ConfigObj, then your checks will look like:: 256 | 257 | keyword = int_list(max=6) 258 | 259 | but the check part will be identical . 260 | 261 | 262 | The check method 263 | ---------------- 264 | 265 | If you're not using ``Validator`` from ConfigObj, then you'll need to call the 266 | ``check`` method yourself. 267 | 268 | If the check fails then it will raise an exception, so you'll want to trap 269 | that. Here's the basic example : 270 | 271 | .. code-block:: python 272 | 273 | from validate import Validator, ValidateError 274 | # 275 | vtor = Validator() 276 | check = "integer(0, 9)" 277 | value = 3 278 | try: 279 | newvalue = vtor.check(check, value) 280 | except ValidateError: 281 | print 'Check Failed.' 282 | else: 283 | print 'Check passed.' 284 | 285 | 286 | .. caution:: 287 | 288 | Although the value can be a string, if it represents a list it should 289 | already have been turned into a list of strings. 290 | 291 | 292 | Default Values 293 | ~~~~~~~~~~~~~~ 294 | 295 | Some values may not be available, and you may want to be able to specify a 296 | default as part of the check. 297 | 298 | You do this by passing the keyword ``missing=True`` to the ``check`` method, as 299 | well as a ``default=value`` in the check. (Constructing these checks is done 300 | automatically by ConfigObj: you only need to know about the ``default=value`` 301 | part) : 302 | 303 | .. code-block:: python 304 | 305 | check1 = 'integer(default=50)' 306 | check2 = 'option("val 1", "val 2", "val 3", default="val 1")' 307 | 308 | assert vtor.check(check1, '', missing=True) == 50 309 | assert vtor.check(check2, '', missing=True) == "val 1" 310 | 311 | 312 | If you pass in ``missing=True`` to the check method, then the actual value is 313 | ignored. If no default is specified in the check, a ``ValidateMissingValue`` 314 | exception is raised. If a default is specified then that is passed to the 315 | check instead. 316 | 317 | If the check has ``default=None`` (case sensitive) then ``vtor.check`` will 318 | *always* return ``None`` (the object). This makes it easy to tell your program 319 | that this check contains no useful value when missing, i.e. the value is 320 | optional, and may be omitted without harm. 321 | 322 | 323 | .. note:: 324 | 325 | As of version 0.3.0, if you specify ``default='None'`` (note the quote marks 326 | around ``None``) then it will be interpreted as the string ``'None'``. 327 | 328 | 329 | List Values 330 | ~~~~~~~~~~~ 331 | 332 | It's possible that you would like your default value to be a list. It's even 333 | possible that you will write your own check functions - and would like to pass 334 | them keyword arguments as lists from within the check. 335 | 336 | To avoid confusing syntax with commas and quotes you use a list constructor to 337 | specify that keyword arguments are lists. This includes the ``default`` value. 338 | This makes checks look something like:: 339 | 340 | checkname(default=list('val1', 'val2', 'val3')) 341 | 342 | 343 | get_default_value 344 | ----------------- 345 | 346 | ``Validator`` instances have a ``get_default_value`` method. It takes a ``check`` string 347 | (the same string you would pass to the ``check`` method) and returns the default value, 348 | converted to the right type. If the check doesn't define a default value then this method 349 | raises a ``KeyError``. 350 | 351 | If the ``check`` has been seen before then it will have been parsed and cached already, 352 | so this method is not expensive to call (however the conversion is done each time). 353 | 354 | 355 | 356 | Validator Exceptions 357 | ==================== 358 | 359 | .. note:: 360 | 361 | If you only use Validator through ConfigObj, it traps these Exceptions for 362 | you. You will still need to know about them for writing your own check 363 | functions. 364 | 365 | ``vtor.check`` indicates that the check has failed by raising an exception. 366 | The appropriate error should be raised in the check function. 367 | 368 | The base error class is ``ValidateError``. All errors (except for ``VdtParamError``) 369 | raised are sub-classes of this. 370 | 371 | If an unrecognised check is specified then ``VdtUnknownCheckError`` is 372 | raised. 373 | 374 | There are also ``VdtTypeError`` and ``VdtValueError``. 375 | 376 | If incorrect parameters are passed to a check function then it will (or should) 377 | raise ``VdtParamError``. As this indicates *programmer* error, rather than an error 378 | in the value, it is a subclass of ``SyntaxError`` instead of ``ValidateError``. 379 | 380 | .. note:: 381 | 382 | This means it *won't* be caught by ConfigObj - but propagated instead. 383 | 384 | If the value supplied is the wrong type, then the check should raise 385 | ``VdtTypeError``. e.g. the check requires the value to be an integer (or 386 | representation of an integer) and something else was supplied. 387 | 388 | If the value supplied is the right type, but an unacceptable value, then the 389 | check should raise ``VdtValueError``. e.g. the check requires the value to 390 | be an integer (or representation of an integer) less than ten and a higher 391 | value was supplied. 392 | 393 | Both ``VdtTypeError`` and ``VdtValueError`` are initialised with the 394 | incorrect value. In other words you raise them like this : 395 | 396 | .. code-block:: python 397 | 398 | raise VdtTypeError(value) 399 | # 400 | raise VdtValueError(value) 401 | 402 | 403 | ``VdtValueError`` has the following subclasses, which should be raised if 404 | they are more appropriate. 405 | 406 | * ``VdtValueTooSmallError`` 407 | * ``VdtValueTooBigError`` 408 | * ``VdtValueTooShortError`` 409 | * ``VdtValueTooLongError`` 410 | 411 | 412 | Writing check functions 413 | ======================= 414 | 415 | Writing check functions is easy. 416 | 417 | The check function will receive the value as its first argument, followed by 418 | any other parameters and keyword arguments. 419 | 420 | If the check fails, it should raise a ``VdtTypeError`` or a 421 | ``VdtValueError`` (or an appropriate subclass). 422 | 423 | All parameters and keyword arguments are *always* passed as strings. (Parsed 424 | from the check string). 425 | 426 | The value might be a string (or list of strings) and need 427 | converting to the right type - alternatively it might already be a list of 428 | integers. Our function needs to be able to handle either. 429 | 430 | If the check passes then it should return the value (possibly converted to the 431 | right type). 432 | 433 | And that's it ! 434 | 435 | 436 | Example 437 | ------- 438 | 439 | Here is an example function that requires a list of integers. Each integer 440 | must be between 0 and 99. 441 | 442 | It takes a single argument specifying the length of the list. (Which allows us 443 | to use the same check in more than one place). If the length can't be converted 444 | to an integer then we need to raise ``VdtParamError``. 445 | 446 | Next we check that the value is a list. Anything else should raise a 447 | ``VdtTypeError``. The list should also have 'length' entries. If the list 448 | has more or less entries then we will need to raise a 449 | ``VdtValueTooShortError`` or a ``VdtValueTooLongError``. 450 | 451 | Then we need to check every entry in the list. Each entry should be an integer 452 | between 0 and 99, or a string representation of an integer between 0 and 99. 453 | Any other type is a ``VdtTypeError``, any other value is a 454 | ``VdtValueError`` (either too big, or too small). 455 | 456 | .. code-block:: python 457 | 458 | def special_list(value, length): 459 | """ 460 | Check that the supplied value is a list of integers, 461 | with 'length' entries, and each entry between 0 and 99. 462 | """ 463 | # length is supplied as a string 464 | # we need to convert it to an integer 465 | try: 466 | length = int(length) 467 | except ValueError: 468 | raise VdtParamError('length', length) 469 | # 470 | # Check the supplied value is a list 471 | if not isinstance(value, list): 472 | raise VdtTypeError(value) 473 | # 474 | # check the length of the list is correct 475 | if len(value) > length: 476 | raise VdtValueTooLongError(value) 477 | elif len(value) < length: 478 | raise VdtValueTooShortError(value) 479 | # 480 | # Next, check every member in the list 481 | # converting strings as necessary 482 | out = [] 483 | for entry in value: 484 | if not isinstance(entry, (str, unicode, int)): 485 | # a value in the list 486 | # is neither an integer nor a string 487 | raise VdtTypeError(value) 488 | elif isinstance(entry, (str, unicode)): 489 | if not entry.isdigit(): 490 | raise VdtTypeError(value) 491 | else: 492 | entry = int(entry) 493 | if entry < 0: 494 | raise VdtValueTooSmallError(value) 495 | elif entry > 99: 496 | raise VdtValueTooBigError(value) 497 | out.append(entry) 498 | # 499 | # if we got this far, all is well 500 | # return the new list 501 | return out 502 | 503 | If you are only using validate from ConfigObj then the error type (*TooBig*, 504 | *TooSmall*, etc) is lost - so you may only want to raise ``VdtValueError``. 505 | 506 | .. caution:: 507 | 508 | If your function raises an exception that isn't a subclass of 509 | ``ValidateError``, then ConfigObj won't trap it. This means validation will 510 | fail. 511 | 512 | This is why our function starts by checking the type of the value. If we 513 | are passed the wrong type (e.g. an integer rather than a list) we get a 514 | ``VdtTypeError`` rather than bombing out when we try to iterate over 515 | the value. 516 | 517 | If you are using validate in another circumstance you may want to create your 518 | own subclasses of ``ValidateError`` which convey more specific information. 519 | 520 | 521 | Known Issues 522 | ============ 523 | 524 | The following parses and then blows up. The resulting error message 525 | is confusing: 526 | 527 | ``checkname(default=list(1, 2, 3, 4)`` 528 | 529 | This is because it parses as: ``checkname(default="list(1", 2, 3, 4)``. 530 | That isn't actually unreasonable, but the error message won't help you 531 | work out what has happened. 532 | 533 | 534 | TODO 535 | ==== 536 | 537 | * A regex check function ? 538 | * A timestamp check function ? (Using the ``parse`` function from ``DateUtil`` perhaps). 539 | 540 | 541 | ISSUES 542 | ====== 543 | 544 | .. note:: 545 | 546 | Please file any bug reports to the `Github Page`_ 547 | 548 | If we could pull tuples out of arguments, it would be easier 549 | to specify arguments for 'mixed_lists'. 550 | 551 | 552 | CHANGELOG 553 | ========= 554 | 555 | 2014/02/08 - Version 2.0.0 556 | -------------------------- 557 | * Python 3 single-source compatibility at the cost of a more restrictive set of versions: 2.6, 2.7, 3.2, 3.3 (otherwise unchanged) 558 | * New maintainers: Rob Dennis and Eli Courtwright 559 | * New home on github 560 | 561 | 2009/10/25 - Version 1.0.1 562 | -------------------------- 563 | 564 | * BUGFIX: Fixed compatibility with Python 2.3. 565 | 566 | 2009/04/13 - Version 1.0.0 567 | -------------------------- 568 | 569 | * BUGFIX: can now handle multiline strings. 570 | * Addition of 'force_list' validation option. 571 | 572 | As the API is stable and there are no known bugs or outstanding feature requests I am marking this 1.0. 573 | 574 | 575 | 2008/02/24 - Version 0.3.2 576 | -------------------------- 577 | 578 | BUGFIX: Handling of None as default value fixed. 579 | 580 | 581 | 2008/02/05 - Version 0.3.1 582 | -------------------------- 583 | 584 | BUGFIX: Unicode checks no longer broken. 585 | 586 | 587 | 2008/02/05 - Version 0.3.0 588 | -------------------------- 589 | 590 | Improved performance with a parse cache. 591 | 592 | New ``get_default_value`` method. Given a check it returns the default 593 | value (converted to the correct type) or raises a ``KeyError`` if the 594 | check doesn't specify a default. 595 | 596 | Added 'tuple' check and corresponding 'is_tuple' function (which always returns a tuple). 597 | 598 | BUGFIX: A quoted 'None' as a default value is no longer treated as None, 599 | but as the string 'None'. 600 | 601 | BUGFIX: We weren't unquoting keyword arguments of length two, so an 602 | empty string didn't work as a default. 603 | 604 | BUGFIX: Strings no longer pass the 'is_list' check. Additionally, the 605 | list checks always return lists. 606 | 607 | A couple of documentation bug fixes. 608 | 609 | Removed CHANGELOG from module. 610 | 611 | 612 | 2007/02/04 Version 0.2.3 613 | ----------------------------- 614 | 615 | Release of 0.2.3 616 | 617 | 618 | 2006/12/17 Version 0.2.3-alpha1 619 | ------------------------------------ 620 | 621 | By Nicola Larosa 622 | 623 | Fixed validate doc to talk of ``boolean`` instead of ``bool``; changed the 624 | ``is_bool`` function to ``is_boolean`` (Sourceforge bug #1531525). 625 | 626 | 627 | 2006/04/29 Version 0.2.2 628 | ----------------------------- 629 | 630 | Addressed bug where a string would pass the ``is_list`` test. (Thanks to 631 | Konrad Wojas.) 632 | 633 | 634 | 2005/12/16 Version 0.2.1 635 | ----------------------------- 636 | 637 | Fixed bug so we can handle keyword argument values with commas. 638 | 639 | We now use a list constructor for passing list values to keyword arguments 640 | (including ``default``):: 641 | 642 | default=list("val", "val", "val") 643 | 644 | Added the ``_test`` test. 645 | 646 | Moved a function call outside a try...except block. 647 | 648 | 649 | 2005/08/18 Version 0.2.0 650 | ----------------------------- 651 | 652 | Updated by Michael Foord and Nicola Larosa 653 | 654 | Does type conversion as well. 655 | 656 | 657 | 2005/02/01 Version 0.1.0 658 | ----------------------------- 659 | 660 | Initial version developed by Michael Foord and Mark Andrews. 661 | 662 | 663 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration for setuptools 3 | # 4 | 5 | [egg_info] 6 | #tag_build = .dev0 7 | tag_date = false 8 | 9 | 10 | [sdist] 11 | formats = gztar 12 | 13 | 14 | [bdist_wheel] 15 | # If you set this to 1, make sure you have a proper Travis CI build matrix, 16 | # and that your Trove classifiers state you support Python 2 and 3 17 | universal = 1 18 | 19 | 20 | [tool:pytest] 21 | norecursedirs = .* *.egg *.egg-info bin dist include lib local share static docs 22 | python_files = src/tests/test_*.py 23 | #addopts = 24 | 25 | 26 | [flake8] 27 | #ignore = E226,… 28 | max-line-length = 132 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # setup.py 3 | # -*- coding: utf-8 -*- 4 | # pylint: disable=invalid-name 5 | 6 | """Install script for ConfigObj""" 7 | 8 | # Copyright (C) 2005-2014: 9 | # (name) : (email) 10 | # Michael Foord: fuzzyman AT voidspace DOT org DOT uk 11 | # Mark Andrews: mark AT la-la DOT com 12 | # Nicola Larosa: nico AT tekNico DOT net 13 | # Rob Dennis: rdennis AT gmail DOT com 14 | # Eli Courtwright: eli AT courtwright DOT org 15 | 16 | # This software is licensed under the terms of the BSD license. 17 | # http://opensource.org/licenses/BSD-3-Clause 18 | import io 19 | import os 20 | import re 21 | import sys 22 | from contextlib import closing 23 | 24 | from setuptools import setup 25 | 26 | if sys.version_info[0] < 2: 27 | print('for Python versions < 3 use configobj ' 28 | 'version 5.0.8') 29 | sys.exit(1) 30 | 31 | __here__ = os.path.abspath(os.path.dirname(__file__)) 32 | 33 | NAME = 'configobj' 34 | MODULES = [] 35 | PACKAGES = ['configobj', 'validate'] 36 | DESCRIPTION = 'Config file reading, writing and validation.' 37 | URL = 'https://github.com/DiffSK/configobj' 38 | 39 | VERSION = '' 40 | with closing(open(os.path.join(__here__, 'src', PACKAGES[0], '_version.py'), 'r')) as handle: 41 | for line in handle.readlines(): 42 | if line.startswith('__version__'): 43 | VERSION = re.split('''['"]''', line)[1] 44 | assert re.match(r"[0-9](\.[0-9]+)", VERSION), "No semantic version found in 'configobj._version'" 45 | 46 | LONG_DESCRIPTION = """**ConfigObj** is a simple but powerful config file reader and writer: an *ini 47 | file round tripper*. Its main feature is that it is very easy to use, with a 48 | straightforward programmer's interface and a simple syntax for config files. 49 | 50 | List of Features 51 | ---------------- 52 | 53 | * Nested sections (subsections), to any level 54 | * List values 55 | * Multiple line values 56 | * Full Unicode support 57 | * String interpolation (substitution) 58 | * Integrated with a powerful validation system 59 | 60 | - including automatic type checking/conversion 61 | - and allowing default values 62 | - repeated sections 63 | 64 | * All comments in the file are preserved 65 | * The order of keys/sections is preserved 66 | * Powerful ``unrepr`` mode for storing/retrieving Python data-types 67 | 68 | """ 69 | 70 | try: 71 | with io.open('CHANGES.rst', encoding='utf-8') as handle: 72 | LONG_DESCRIPTION += handle.read() 73 | except EnvironmentError as exc: 74 | # Build / install anyway 75 | print("WARNING: Cannot open/read CHANGES.rst due to {}".format(exc)) 76 | 77 | CLASSIFIERS = [ 78 | # Details at http://pypi.python.org/pypi?:action=list_classifiers 79 | 'Development Status :: 6 - Mature', 80 | 'Intended Audience :: Developers', 81 | 'License :: OSI Approved :: BSD License', 82 | 'Programming Language :: Python', 83 | 'Programming Language :: Python :: 3', 84 | 'Programming Language :: Python :: 3.8', 85 | 'Programming Language :: Python :: 3.9', 86 | 'Programming Language :: Python :: 3.10', 87 | 'Programming Language :: Python :: 3.11', 88 | 'Programming Language :: Python :: 3.12', 89 | 'Programming Language :: Python :: 3.13', 90 | 'Operating System :: OS Independent', 91 | 'Topic :: Software Development :: Libraries', 92 | 'Topic :: Software Development :: Libraries :: Python Modules', 93 | ] 94 | 95 | AUTHOR = 'Rob Dennis, Eli Courtwright (Michael Foord & Nicola Larosa original maintainers)' 96 | 97 | AUTHOR_EMAIL = 'rdennis+configobj@gmail.com, eli@courtwright.org, michael@python.org, nico@tekNico.net' 98 | 99 | KEYWORDS = "config, ini, dictionary, application, admin, sysadmin, configuration, validation".split(', ') 100 | 101 | project = dict( 102 | name=NAME, 103 | version=VERSION, 104 | description=DESCRIPTION, 105 | long_description=LONG_DESCRIPTION, 106 | author=AUTHOR, 107 | author_email=AUTHOR_EMAIL, 108 | url=URL, 109 | py_modules=MODULES, 110 | package_dir={'': 'src'}, 111 | packages=PACKAGES, 112 | python_requires='>=3.8', 113 | classifiers=CLASSIFIERS, 114 | keywords=KEYWORDS, 115 | license='BSD-3-Clause', 116 | ) 117 | 118 | if __name__ == '__main__': 119 | setup(**project) 120 | -------------------------------------------------------------------------------- /setup_validate.py: -------------------------------------------------------------------------------- 1 | # setup.py 2 | # Install script for validate 3 | # Copyright (C) 2005-2014: 4 | # (name) : (email) 5 | # Michael Foord: fuzzyman AT voidspace DOT org DOT uk 6 | # Mark Andrews: mark AT la-la DOT com 7 | # Nicola Larosa: nico AT tekNico DOT net 8 | # Rob Dennis: rdennis AT gmail DOT com 9 | # Eli Courtwright: eli AT courtwright DOT org 10 | 11 | # This software is licensed under the terms of the BSD license. 12 | # http://opensource.org/licenses/BSD-3-Clause 13 | 14 | import sys 15 | from distutils.core import setup 16 | from validate import __version__ as VERSION 17 | 18 | NAME = 'validate' 19 | 20 | MODULES = 'validate', 21 | 22 | DESCRIPTION = 'Config file reading, writing, and validation.' 23 | 24 | URL = 'http://www.voidspace.org.uk/python/validate.html' 25 | 26 | DOWNLOAD_URL = "http://www.voidspace.org.uk/downloads/validate.py" 27 | 28 | LONG_DESCRIPTION = """`validate.py `_ is a 29 | module for validating values against a specification. It can be used 30 | with `ConfigObj `_, or as a 31 | standalone module. 32 | 33 | It is extensible, and as well as doing type conversion from strings, 34 | you can easily implement your own functions for transforming values in 35 | any way you please.""" 36 | 37 | CLASSIFIERS = [ 38 | 'Development Status :: 5 - Production/Stable', 39 | 'Intended Audience :: Developers', 40 | 'License :: OSI Approved :: BSD License', 41 | 'Programming Language :: Python', 42 | 'Operating System :: OS Independent', 43 | 'Topic :: Software Development :: Libraries', 44 | 'Topic :: Software Development :: Libraries :: Python Modules', 45 | ] 46 | 47 | AUTHOR = 'Michael Foord' 48 | 49 | AUTHOR_EMAIL = 'fuzzyman@voidspace.org.uk' 50 | 51 | KEYWORDS = "validation, schema, conversion, checking, configobj, config, configuration".split(', ') 52 | 53 | 54 | setup(name=NAME, 55 | version=VERSION, 56 | description=DESCRIPTION, 57 | long_description=LONG_DESCRIPTION, 58 | download_url=DOWNLOAD_URL, 59 | author=AUTHOR, 60 | author_email=AUTHOR_EMAIL, 61 | url=URL, 62 | py_modules=MODULES, 63 | classifiers=CLASSIFIERS, 64 | keywords=KEYWORDS 65 | ) 66 | -------------------------------------------------------------------------------- /src/configobj/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = '5.0.9' 2 | -------------------------------------------------------------------------------- /src/configobj/validate.py: -------------------------------------------------------------------------------- 1 | # validate.py 2 | # A Validator object 3 | # Copyright (C) 2005-2014: 4 | # (name) : (email) 5 | # Michael Foord: fuzzyman AT voidspace DOT org DOT uk 6 | # Mark Andrews: mark AT la-la DOT com 7 | # Nicola Larosa: nico AT tekNico DOT net 8 | # Rob Dennis: rdennis AT gmail DOT com 9 | # Eli Courtwright: eli AT courtwright DOT org 10 | 11 | # This software is licensed under the terms of the BSD license. 12 | # http://opensource.org/licenses/BSD-3-Clause 13 | 14 | # ConfigObj 5 - main repository for documentation and issue tracking: 15 | # https://github.com/DiffSK/configobj 16 | 17 | """ 18 | The Validator object is used to check that supplied values 19 | conform to a specification. 20 | 21 | The value can be supplied as a string - e.g. from a config file. 22 | In this case the check will also *convert* the value to 23 | the required type. This allows you to add validation 24 | as a transparent layer to access data stored as strings. 25 | The validation checks that the data is correct *and* 26 | converts it to the expected type. 27 | 28 | Some standard checks are provided for basic data types. 29 | Additional checks are easy to write. They can be 30 | provided when the ``Validator`` is instantiated or 31 | added afterwards. 32 | 33 | The standard functions work with the following basic data types : 34 | 35 | * integers 36 | * floats 37 | * booleans 38 | * strings 39 | * ip_addr 40 | 41 | plus lists of these datatypes 42 | 43 | Adding additional checks is done through coding simple functions. 44 | 45 | The full set of standard checks are : 46 | 47 | * 'integer': matches integer values (including negative) 48 | Takes optional 'min' and 'max' arguments : :: 49 | 50 | integer() 51 | integer(3, 9) # any value from 3 to 9 52 | integer(min=0) # any positive value 53 | integer(max=9) 54 | 55 | * 'float': matches float values 56 | Has the same parameters as the integer check. 57 | 58 | * 'boolean': matches boolean values - ``True`` or ``False`` 59 | Acceptable string values for True are : 60 | true, on, yes, 1 61 | Acceptable string values for False are : 62 | false, off, no, 0 63 | 64 | Any other value raises an error. 65 | 66 | * 'ip_addr': matches an Internet Protocol address, v.4, represented 67 | by a dotted-quad string, i.e. '1.2.3.4'. 68 | 69 | * 'string': matches any string. 70 | Takes optional keyword args 'min' and 'max' 71 | to specify min and max lengths of the string. 72 | 73 | * 'list': matches any list. 74 | Takes optional keyword args 'min', and 'max' to specify min and 75 | max sizes of the list. (Always returns a list.) 76 | 77 | * 'tuple': matches any tuple. 78 | Takes optional keyword args 'min', and 'max' to specify min and 79 | max sizes of the tuple. (Always returns a tuple.) 80 | 81 | * 'int_list': Matches a list of integers. 82 | Takes the same arguments as list. 83 | 84 | * 'float_list': Matches a list of floats. 85 | Takes the same arguments as list. 86 | 87 | * 'bool_list': Matches a list of boolean values. 88 | Takes the same arguments as list. 89 | 90 | * 'ip_addr_list': Matches a list of IP addresses. 91 | Takes the same arguments as list. 92 | 93 | * 'string_list': Matches a list of strings. 94 | Takes the same arguments as list. 95 | 96 | * 'mixed_list': Matches a list with different types in 97 | specific positions. List size must match 98 | the number of arguments. 99 | 100 | Each position can be one of : 101 | 'integer', 'float', 'ip_addr', 'string', 'boolean' 102 | 103 | So to specify a list with two strings followed 104 | by two integers, you write the check as : :: 105 | 106 | mixed_list('string', 'string', 'integer', 'integer') 107 | 108 | * 'pass': This check matches everything ! It never fails 109 | and the value is unchanged. 110 | 111 | It is also the default if no check is specified. 112 | 113 | * 'option': This check matches any from a list of options. 114 | You specify this check with : :: 115 | 116 | option('option 1', 'option 2', 'option 3') 117 | 118 | You can supply a default value (returned if no value is supplied) 119 | using the default keyword argument. 120 | 121 | You specify a list argument for default using a list constructor syntax in 122 | the check : :: 123 | 124 | checkname(arg1, arg2, default=list('val 1', 'val 2', 'val 3')) 125 | 126 | A badly formatted set of arguments will raise a ``VdtParamError``. 127 | """ 128 | 129 | __version__ = '1.0.1' 130 | 131 | 132 | __all__ = ( 133 | '__version__', 134 | 'dottedQuadToNum', 135 | 'numToDottedQuad', 136 | 'ValidateError', 137 | 'VdtUnknownCheckError', 138 | 'VdtParamError', 139 | 'VdtTypeError', 140 | 'VdtValueError', 141 | 'VdtValueTooSmallError', 142 | 'VdtValueTooBigError', 143 | 'VdtValueTooShortError', 144 | 'VdtValueTooLongError', 145 | 'VdtMissingValue', 146 | 'Validator', 147 | 'is_integer', 148 | 'is_float', 149 | 'is_boolean', 150 | 'is_list', 151 | 'is_tuple', 152 | 'is_ip_addr', 153 | 'is_string', 154 | 'is_int_list', 155 | 'is_bool_list', 156 | 'is_float_list', 157 | 'is_string_list', 158 | 'is_ip_addr_list', 159 | 'is_mixed_list', 160 | 'is_option', 161 | ) 162 | 163 | 164 | import re 165 | import sys 166 | from pprint import pprint 167 | 168 | 169 | _list_arg = re.compile(r''' 170 | (?: 171 | ([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*list\( 172 | ( 173 | (?: 174 | \s* 175 | (?: 176 | (?:".*?")| # double quotes 177 | (?:'.*?')| # single quotes 178 | (?:[^'",\s\)][^,\)]*?) # unquoted 179 | ) 180 | \s*,\s* 181 | )* 182 | (?: 183 | (?:".*?")| # double quotes 184 | (?:'.*?')| # single quotes 185 | (?:[^'",\s\)][^,\)]*?) # unquoted 186 | )? # last one 187 | ) 188 | \) 189 | ) 190 | ''', re.VERBOSE | re.DOTALL) # two groups 191 | 192 | _list_members = re.compile(r''' 193 | ( 194 | (?:".*?")| # double quotes 195 | (?:'.*?')| # single quotes 196 | (?:[^'",\s=][^,=]*?) # unquoted 197 | ) 198 | (?: 199 | (?:\s*,\s*)|(?:\s*$) # comma 200 | ) 201 | ''', re.VERBOSE | re.DOTALL) # one group 202 | 203 | _paramstring = r''' 204 | (?: 205 | ( 206 | (?: 207 | [a-zA-Z_][a-zA-Z0-9_]*\s*=\s*list\( 208 | (?: 209 | \s* 210 | (?: 211 | (?:".*?")| # double quotes 212 | (?:'.*?')| # single quotes 213 | (?:[^'",\s\)][^,\)]*?) # unquoted 214 | ) 215 | \s*,\s* 216 | )* 217 | (?: 218 | (?:".*?")| # double quotes 219 | (?:'.*?')| # single quotes 220 | (?:[^'",\s\)][^,\)]*?) # unquoted 221 | )? # last one 222 | \) 223 | )| 224 | (?: 225 | (?:".*?")| # double quotes 226 | (?:'.*?')| # single quotes 227 | (?:[^'",\s=][^,=]*?)| # unquoted 228 | (?: # keyword argument 229 | [a-zA-Z_][a-zA-Z0-9_]*\s*=\s* 230 | (?: 231 | (?:".*?")| # double quotes 232 | (?:'.*?')| # single quotes 233 | (?:[^'",\s=][^,=]*?) # unquoted 234 | ) 235 | ) 236 | ) 237 | ) 238 | (?: 239 | (?:\s*,\s*)|(?:\s*$) # comma 240 | ) 241 | ) 242 | ''' 243 | 244 | _matchstring = '^%s*' % _paramstring 245 | 246 | 247 | def dottedQuadToNum(ip): 248 | """ 249 | Convert decimal dotted quad string to long integer 250 | 251 | >>> int(dottedQuadToNum('1 ')) 252 | 1 253 | >>> int(dottedQuadToNum(' 1.2')) 254 | 16777218 255 | >>> int(dottedQuadToNum(' 1.2.3 ')) 256 | 16908291 257 | >>> int(dottedQuadToNum('1.2.3.4')) 258 | 16909060 259 | >>> dottedQuadToNum('255.255.255.255') 260 | 4294967295 261 | >>> dottedQuadToNum('255.255.255.256') 262 | Traceback (most recent call last): 263 | ValueError: Not a good dotted-quad IP: 255.255.255.256 264 | """ 265 | 266 | # import here to avoid it when ip_addr values are not used 267 | import socket, struct 268 | 269 | try: 270 | return struct.unpack('!L', 271 | socket.inet_aton(ip.strip()))[0] 272 | except socket.error: 273 | raise ValueError('Not a good dotted-quad IP: %s' % ip) 274 | return 275 | 276 | 277 | def numToDottedQuad(num): 278 | """ 279 | Convert int or long int to dotted quad string 280 | 281 | >>> numToDottedQuad(int(-1)) 282 | Traceback (most recent call last): 283 | ValueError: Not a good numeric IP: -1 284 | >>> numToDottedQuad(int(1)) 285 | '0.0.0.1' 286 | >>> numToDottedQuad(int(16777218)) 287 | '1.0.0.2' 288 | >>> numToDottedQuad(int(16908291)) 289 | '1.2.0.3' 290 | >>> numToDottedQuad(int(16909060)) 291 | '1.2.3.4' 292 | >>> numToDottedQuad(int(4294967295)) 293 | '255.255.255.255' 294 | >>> numToDottedQuad(int(4294967296)) 295 | Traceback (most recent call last): 296 | ValueError: Not a good numeric IP: 4294967296 297 | >>> numToDottedQuad(-1) 298 | Traceback (most recent call last): 299 | ValueError: Not a good numeric IP: -1 300 | >>> numToDottedQuad(1) 301 | '0.0.0.1' 302 | >>> numToDottedQuad(16777218) 303 | '1.0.0.2' 304 | >>> numToDottedQuad(16908291) 305 | '1.2.0.3' 306 | >>> numToDottedQuad(16909060) 307 | '1.2.3.4' 308 | >>> numToDottedQuad(4294967295) 309 | '255.255.255.255' 310 | >>> numToDottedQuad(4294967296) 311 | Traceback (most recent call last): 312 | ValueError: Not a good numeric IP: 4294967296 313 | 314 | """ 315 | 316 | # import here to avoid it when ip_addr values are not used 317 | import socket, struct 318 | 319 | # no need to intercept here, 4294967295L is fine 320 | if num > int(4294967295) or num < 0: 321 | raise ValueError('Not a good numeric IP: %s' % num) 322 | try: 323 | return socket.inet_ntoa( 324 | struct.pack('!L', int(num))) 325 | except (socket.error, struct.error, OverflowError): 326 | raise ValueError('Not a good numeric IP: %s' % num) 327 | 328 | 329 | class ValidateError(Exception): 330 | """ 331 | This error indicates that the check failed. 332 | It can be the base class for more specific errors. 333 | 334 | Any check function that fails ought to raise this error. 335 | (or a subclass) 336 | 337 | >>> raise ValidateError 338 | Traceback (most recent call last): 339 | ValidateError 340 | """ 341 | 342 | 343 | class VdtMissingValue(ValidateError): 344 | """No value was supplied to a check that needed one.""" 345 | 346 | 347 | class VdtUnknownCheckError(ValidateError): 348 | """An unknown check function was requested""" 349 | 350 | def __init__(self, value): 351 | """ 352 | >>> raise VdtUnknownCheckError('yoda') 353 | Traceback (most recent call last): 354 | VdtUnknownCheckError: the check "yoda" is unknown. 355 | """ 356 | ValidateError.__init__(self, 'the check "%s" is unknown.' % (value,)) 357 | 358 | 359 | class VdtParamError(SyntaxError): 360 | """An incorrect parameter was passed""" 361 | 362 | def __init__(self, name, value): 363 | """ 364 | >>> raise VdtParamError('yoda', 'jedi') 365 | Traceback (most recent call last): 366 | VdtParamError: passed an incorrect value "jedi" for parameter "yoda". 367 | """ 368 | SyntaxError.__init__(self, 'passed an incorrect value "%s" for parameter "%s".' % (value, name)) 369 | 370 | 371 | class VdtTypeError(ValidateError): 372 | """The value supplied was of the wrong type""" 373 | 374 | def __init__(self, value): 375 | """ 376 | >>> raise VdtTypeError('jedi') 377 | Traceback (most recent call last): 378 | VdtTypeError: the value "jedi" is of the wrong type. 379 | """ 380 | ValidateError.__init__(self, 'the value "%s" is of the wrong type.' % (value,)) 381 | 382 | 383 | class VdtValueError(ValidateError): 384 | """The value supplied was of the correct type, but was not an allowed value.""" 385 | 386 | def __init__(self, value): 387 | """ 388 | >>> raise VdtValueError('jedi') 389 | Traceback (most recent call last): 390 | VdtValueError: the value "jedi" is unacceptable. 391 | """ 392 | ValidateError.__init__(self, 'the value "%s" is unacceptable.' % (value,)) 393 | 394 | 395 | class VdtValueTooSmallError(VdtValueError): 396 | """The value supplied was of the correct type, but was too small.""" 397 | 398 | def __init__(self, value): 399 | """ 400 | >>> raise VdtValueTooSmallError('0') 401 | Traceback (most recent call last): 402 | VdtValueTooSmallError: the value "0" is too small. 403 | """ 404 | ValidateError.__init__(self, 'the value "%s" is too small.' % (value,)) 405 | 406 | 407 | class VdtValueTooBigError(VdtValueError): 408 | """The value supplied was of the correct type, but was too big.""" 409 | 410 | def __init__(self, value): 411 | """ 412 | >>> raise VdtValueTooBigError('1') 413 | Traceback (most recent call last): 414 | VdtValueTooBigError: the value "1" is too big. 415 | """ 416 | ValidateError.__init__(self, 'the value "%s" is too big.' % (value,)) 417 | 418 | 419 | class VdtValueTooShortError(VdtValueError): 420 | """The value supplied was of the correct type, but was too short.""" 421 | 422 | def __init__(self, value): 423 | """ 424 | >>> raise VdtValueTooShortError('jed') 425 | Traceback (most recent call last): 426 | VdtValueTooShortError: the value "jed" is too short. 427 | """ 428 | ValidateError.__init__( 429 | self, 430 | 'the value "%s" is too short.' % (value,)) 431 | 432 | 433 | class VdtValueTooLongError(VdtValueError): 434 | """The value supplied was of the correct type, but was too long.""" 435 | 436 | def __init__(self, value): 437 | """ 438 | >>> raise VdtValueTooLongError('jedie') 439 | Traceback (most recent call last): 440 | VdtValueTooLongError: the value "jedie" is too long. 441 | """ 442 | ValidateError.__init__(self, 'the value "%s" is too long.' % (value,)) 443 | 444 | 445 | class Validator(object): 446 | """ 447 | Validator is an object that allows you to register a set of 'checks'. 448 | These checks take input and test that it conforms to the check. 449 | 450 | This can also involve converting the value from a string into 451 | the correct datatype. 452 | 453 | The ``check`` method takes an input string which configures which 454 | check is to be used and applies that check to a supplied value. 455 | 456 | An example input string would be: 457 | 'int_range(param1, param2)' 458 | 459 | You would then provide something like: 460 | 461 | >>> def int_range_check(value, min, max): 462 | ... # turn min and max from strings to integers 463 | ... min = int(min) 464 | ... max = int(max) 465 | ... # check that value is of the correct type. 466 | ... # possible valid inputs are integers or strings 467 | ... # that represent integers 468 | ... if not isinstance(value, (int, str)): 469 | ... raise VdtTypeError(value) 470 | ... elif isinstance(value, str): 471 | ... # if we are given a string 472 | ... # attempt to convert to an integer 473 | ... try: 474 | ... value = int(value) 475 | ... except ValueError: 476 | ... raise VdtValueError(value) 477 | ... # check the value is between our constraints 478 | ... if not min <= value: 479 | ... raise VdtValueTooSmallError(value) 480 | ... if not value <= max: 481 | ... raise VdtValueTooBigError(value) 482 | ... return value 483 | 484 | >>> fdict = {'int_range': int_range_check} 485 | >>> vtr1 = Validator(fdict) 486 | >>> vtr1.check('int_range(20, 40)', '30') 487 | 30 488 | >>> vtr1.check('int_range(20, 40)', '60') 489 | Traceback (most recent call last): 490 | VdtValueTooBigError: the value "60" is too big. 491 | 492 | New functions can be added with : :: 493 | 494 | >>> vtr2 = Validator() 495 | >>> vtr2.functions['int_range'] = int_range_check 496 | 497 | Or by passing in a dictionary of functions when Validator 498 | is instantiated. 499 | 500 | Your functions *can* use keyword arguments, 501 | but the first argument should always be 'value'. 502 | 503 | If the function doesn't take additional arguments, 504 | the parentheses are optional in the check. 505 | It can be written with either of : :: 506 | 507 | keyword = function_name 508 | keyword = function_name() 509 | 510 | The first program to utilise Validator() was Michael Foord's 511 | ConfigObj, an alternative to ConfigParser which supports lists and 512 | can validate a config file using a config schema. 513 | For more details on using Validator with ConfigObj see: 514 | https://configobj.readthedocs.org/en/latest/configobj.html 515 | """ 516 | 517 | # this regex does the initial parsing of the checks 518 | _func_re = re.compile(r'([^\(\)]+?)\((.*)\)', re.DOTALL) 519 | 520 | # this regex takes apart keyword arguments 521 | _key_arg = re.compile(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$', re.DOTALL) 522 | 523 | 524 | # this regex finds keyword=list(....) type values 525 | _list_arg = _list_arg 526 | 527 | # this regex takes individual values out of lists - in one pass 528 | _list_members = _list_members 529 | 530 | # These regexes check a set of arguments for validity 531 | # and then pull the members out 532 | _paramfinder = re.compile(_paramstring, re.VERBOSE | re.DOTALL) 533 | _matchfinder = re.compile(_matchstring, re.VERBOSE | re.DOTALL) 534 | 535 | 536 | def __init__(self, functions=None): 537 | """ 538 | >>> vtri = Validator() 539 | """ 540 | self.functions = { 541 | '': self._pass, 542 | 'integer': is_integer, 543 | 'float': is_float, 544 | 'boolean': is_boolean, 545 | 'ip_addr': is_ip_addr, 546 | 'string': is_string, 547 | 'list': is_list, 548 | 'tuple': is_tuple, 549 | 'int_list': is_int_list, 550 | 'float_list': is_float_list, 551 | 'bool_list': is_bool_list, 552 | 'ip_addr_list': is_ip_addr_list, 553 | 'string_list': is_string_list, 554 | 'mixed_list': is_mixed_list, 555 | 'pass': self._pass, 556 | 'option': is_option, 557 | 'force_list': force_list, 558 | } 559 | if functions is not None: 560 | self.functions.update(functions) 561 | # tekNico: for use by ConfigObj 562 | self.baseErrorClass = ValidateError 563 | self._cache = {} 564 | 565 | 566 | def check(self, check, value, missing=False): 567 | """ 568 | Usage: check(check, value) 569 | 570 | Arguments: 571 | check: string representing check to apply (including arguments) 572 | value: object to be checked 573 | Returns value, converted to correct type if necessary 574 | 575 | If the check fails, raises a ``ValidateError`` subclass. 576 | 577 | >>> vtor.check('yoda', '') 578 | Traceback (most recent call last): 579 | VdtUnknownCheckError: the check "yoda" is unknown. 580 | >>> vtor.check('yoda()', '') 581 | Traceback (most recent call last): 582 | VdtUnknownCheckError: the check "yoda" is unknown. 583 | 584 | >>> vtor.check('string(default="")', '', missing=True) 585 | '' 586 | """ 587 | fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check) 588 | 589 | if missing: 590 | if default is None: 591 | # no information needed here - to be handled by caller 592 | raise VdtMissingValue() 593 | value = self._handle_none(default) 594 | 595 | if value is None: 596 | return None 597 | 598 | return self._check_value(value, fun_name, fun_args, fun_kwargs) 599 | 600 | 601 | def _handle_none(self, value): 602 | if value == 'None': 603 | return None 604 | elif value in ("'None'", '"None"'): 605 | # Special case a quoted None 606 | value = self._unquote(value) 607 | return value 608 | 609 | 610 | def _parse_with_caching(self, check): 611 | if check in self._cache: 612 | fun_name, fun_args, fun_kwargs, default = self._cache[check] 613 | # We call list and dict below to work with *copies* of the data 614 | # rather than the original (which are mutable of course) 615 | fun_args = list(fun_args) 616 | fun_kwargs = dict(fun_kwargs) 617 | else: 618 | fun_name, fun_args, fun_kwargs, default = self._parse_check(check) 619 | fun_kwargs = dict([(str(key), value) for (key, value) in list(fun_kwargs.items())]) 620 | self._cache[check] = fun_name, list(fun_args), dict(fun_kwargs), default 621 | return fun_name, fun_args, fun_kwargs, default 622 | 623 | 624 | def _check_value(self, value, fun_name, fun_args, fun_kwargs): 625 | try: 626 | fun = self.functions[fun_name] 627 | except KeyError: 628 | raise VdtUnknownCheckError(fun_name) 629 | else: 630 | return fun(value, *fun_args, **fun_kwargs) 631 | 632 | 633 | def _parse_check(self, check): 634 | fun_match = self._func_re.match(check) 635 | if fun_match: 636 | fun_name = fun_match.group(1) 637 | arg_string = fun_match.group(2) 638 | arg_match = self._matchfinder.match(arg_string) 639 | if arg_match is None: 640 | # Bad syntax 641 | raise VdtParamError('Bad syntax in check "%s".' % check) 642 | fun_args = [] 643 | fun_kwargs = {} 644 | # pull out args of group 2 645 | for arg in self._paramfinder.findall(arg_string): 646 | # args may need whitespace removing (before removing quotes) 647 | arg = arg.strip() 648 | listmatch = self._list_arg.match(arg) 649 | if listmatch: 650 | key, val = self._list_handle(listmatch) 651 | fun_kwargs[key] = val 652 | continue 653 | keymatch = self._key_arg.match(arg) 654 | if keymatch: 655 | val = keymatch.group(2) 656 | if not val in ("'None'", '"None"'): 657 | # Special case a quoted None 658 | val = self._unquote(val) 659 | fun_kwargs[keymatch.group(1)] = val 660 | continue 661 | 662 | fun_args.append(self._unquote(arg)) 663 | else: 664 | # allows for function names without (args) 665 | return check, (), {}, None 666 | 667 | # Default must be deleted if the value is specified too, 668 | # otherwise the check function will get a spurious "default" keyword arg 669 | default = fun_kwargs.pop('default', None) 670 | return fun_name, fun_args, fun_kwargs, default 671 | 672 | 673 | def _unquote(self, val): 674 | """Unquote a value if necessary.""" 675 | if (len(val) >= 2) and (val[0] in ("'", '"')) and (val[0] == val[-1]): 676 | val = val[1:-1] 677 | return val 678 | 679 | 680 | def _list_handle(self, listmatch): 681 | """Take apart a ``keyword=list('val, 'val')`` type string.""" 682 | out = [] 683 | name = listmatch.group(1) 684 | args = listmatch.group(2) 685 | for arg in self._list_members.findall(args): 686 | out.append(self._unquote(arg)) 687 | return name, out 688 | 689 | 690 | def _pass(self, value): 691 | """ 692 | Dummy check that always passes 693 | 694 | >>> vtor.check('', 0) 695 | 0 696 | >>> vtor.check('', '0') 697 | '0' 698 | """ 699 | return value 700 | 701 | 702 | def get_default_value(self, check): 703 | """ 704 | Given a check, return the default value for the check 705 | (converted to the right type). 706 | 707 | If the check doesn't specify a default value then a 708 | ``KeyError`` will be raised. 709 | """ 710 | fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check) 711 | if default is None: 712 | raise KeyError('Check "%s" has no default value.' % check) 713 | value = self._handle_none(default) 714 | if value is None: 715 | return value 716 | return self._check_value(value, fun_name, fun_args, fun_kwargs) 717 | 718 | 719 | def _is_num_param(names, values, to_float=False): 720 | """ 721 | Return numbers from inputs or raise VdtParamError. 722 | 723 | Lets ``None`` pass through. 724 | Pass in keyword argument ``to_float=True`` to 725 | use float for the conversion rather than int. 726 | 727 | >>> _is_num_param(('', ''), (0, 1.0)) 728 | [0, 1] 729 | >>> _is_num_param(('', ''), (0, 1.0), to_float=True) 730 | [0.0, 1.0] 731 | >>> _is_num_param(('a'), ('a')) 732 | Traceback (most recent call last): 733 | VdtParamError: passed an incorrect value "a" for parameter "a". 734 | """ 735 | fun = to_float and float or int 736 | out_params = [] 737 | for (name, val) in zip(names, values): 738 | if val is None: 739 | out_params.append(val) 740 | elif isinstance(val, (int, float, str)): 741 | try: 742 | out_params.append(fun(val)) 743 | except ValueError as e: 744 | raise VdtParamError(name, val) 745 | else: 746 | raise VdtParamError(name, val) 747 | return out_params 748 | 749 | 750 | # built in checks 751 | # you can override these by setting the appropriate name 752 | # in Validator.functions 753 | # note: if the params are specified wrongly in your input string, 754 | # you will also raise errors. 755 | 756 | def is_integer(value, min=None, max=None): 757 | """ 758 | A check that tests that a given value is an integer (int) 759 | and optionally, between bounds. A negative value is accepted, while 760 | a float will fail. 761 | 762 | If the value is a string, then the conversion is done - if possible. 763 | Otherwise a VdtError is raised. 764 | 765 | >>> vtor.check('integer', '-1') 766 | -1 767 | >>> vtor.check('integer', '0') 768 | 0 769 | >>> vtor.check('integer', 9) 770 | 9 771 | >>> vtor.check('integer', 'a') 772 | Traceback (most recent call last): 773 | VdtTypeError: the value "a" is of the wrong type. 774 | >>> vtor.check('integer', '2.2') 775 | Traceback (most recent call last): 776 | VdtTypeError: the value "2.2" is of the wrong type. 777 | >>> vtor.check('integer(10)', '20') 778 | 20 779 | >>> vtor.check('integer(max=20)', '15') 780 | 15 781 | >>> vtor.check('integer(10)', '9') 782 | Traceback (most recent call last): 783 | VdtValueTooSmallError: the value "9" is too small. 784 | >>> vtor.check('integer(10)', 9) 785 | Traceback (most recent call last): 786 | VdtValueTooSmallError: the value "9" is too small. 787 | >>> vtor.check('integer(max=20)', '35') 788 | Traceback (most recent call last): 789 | VdtValueTooBigError: the value "35" is too big. 790 | >>> vtor.check('integer(max=20)', 35) 791 | Traceback (most recent call last): 792 | VdtValueTooBigError: the value "35" is too big. 793 | >>> vtor.check('integer(0, 9)', False) 794 | 0 795 | """ 796 | (min_val, max_val) = _is_num_param(('min', 'max'), (min, max)) 797 | if not isinstance(value, (int, str)): 798 | raise VdtTypeError(value) 799 | if isinstance(value, str): 800 | # if it's a string - does it represent an integer ? 801 | try: 802 | value = int(value) 803 | except ValueError: 804 | raise VdtTypeError(value) 805 | if (min_val is not None) and (value < min_val): 806 | raise VdtValueTooSmallError(value) 807 | if (max_val is not None) and (value > max_val): 808 | raise VdtValueTooBigError(value) 809 | return value 810 | 811 | 812 | def is_float(value, min=None, max=None): 813 | """ 814 | A check that tests that a given value is a float 815 | (an integer will be accepted), and optionally - that it is between bounds. 816 | 817 | If the value is a string, then the conversion is done - if possible. 818 | Otherwise a VdtError is raised. 819 | 820 | This can accept negative values. 821 | 822 | >>> vtor.check('float', '2') 823 | 2.0 824 | 825 | From now on we multiply the value to avoid comparing decimals 826 | 827 | >>> vtor.check('float', '-6.8') * 10 828 | -68.0 829 | >>> vtor.check('float', '12.2') * 10 830 | 122.0 831 | >>> vtor.check('float', 8.4) * 10 832 | 84.0 833 | >>> vtor.check('float', 'a') 834 | Traceback (most recent call last): 835 | VdtTypeError: the value "a" is of the wrong type. 836 | >>> vtor.check('float(10.1)', '10.2') * 10 837 | 102.0 838 | >>> vtor.check('float(max=20.2)', '15.1') * 10 839 | 151.0 840 | >>> vtor.check('float(10.0)', '9.0') 841 | Traceback (most recent call last): 842 | VdtValueTooSmallError: the value "9.0" is too small. 843 | >>> vtor.check('float(max=20.0)', '35.0') 844 | Traceback (most recent call last): 845 | VdtValueTooBigError: the value "35.0" is too big. 846 | """ 847 | (min_val, max_val) = _is_num_param( 848 | ('min', 'max'), (min, max), to_float=True) 849 | if not isinstance(value, (int, float, str)): 850 | raise VdtTypeError(value) 851 | if not isinstance(value, float): 852 | # if it's a string - does it represent a float ? 853 | try: 854 | value = float(value) 855 | except ValueError: 856 | raise VdtTypeError(value) 857 | if (min_val is not None) and (value < min_val): 858 | raise VdtValueTooSmallError(value) 859 | if (max_val is not None) and (value > max_val): 860 | raise VdtValueTooBigError(value) 861 | return value 862 | 863 | 864 | bool_dict = { 865 | True: True, 'on': True, '1': True, 'true': True, 'yes': True, 866 | False: False, 'off': False, '0': False, 'false': False, 'no': False, 867 | } 868 | 869 | 870 | def is_boolean(value): 871 | """ 872 | Check if the value represents a boolean. 873 | 874 | >>> vtor.check('boolean', 0) 875 | 0 876 | >>> vtor.check('boolean', False) 877 | 0 878 | >>> vtor.check('boolean', '0') 879 | 0 880 | >>> vtor.check('boolean', 'off') 881 | 0 882 | >>> vtor.check('boolean', 'false') 883 | 0 884 | >>> vtor.check('boolean', 'no') 885 | 0 886 | >>> vtor.check('boolean', 'nO') 887 | 0 888 | >>> vtor.check('boolean', 'NO') 889 | 0 890 | >>> vtor.check('boolean', 1) 891 | 1 892 | >>> vtor.check('boolean', True) 893 | 1 894 | >>> vtor.check('boolean', '1') 895 | 1 896 | >>> vtor.check('boolean', 'on') 897 | 1 898 | >>> vtor.check('boolean', 'true') 899 | 1 900 | >>> vtor.check('boolean', 'yes') 901 | 1 902 | >>> vtor.check('boolean', 'Yes') 903 | 1 904 | >>> vtor.check('boolean', 'YES') 905 | 1 906 | >>> vtor.check('boolean', '') 907 | Traceback (most recent call last): 908 | VdtTypeError: the value "" is of the wrong type. 909 | >>> vtor.check('boolean', 'up') 910 | Traceback (most recent call last): 911 | VdtTypeError: the value "up" is of the wrong type. 912 | 913 | """ 914 | if isinstance(value, str): 915 | try: 916 | return bool_dict[value.lower()] 917 | except KeyError: 918 | raise VdtTypeError(value) 919 | # we do an equality test rather than an identity test 920 | # this ensures Python 2.2 compatibilty 921 | # and allows 0 and 1 to represent True and False 922 | if value == False: 923 | return False 924 | elif value == True: 925 | return True 926 | else: 927 | raise VdtTypeError(value) 928 | 929 | 930 | def is_ip_addr(value): 931 | """ 932 | Check that the supplied value is an Internet Protocol address, v.4, 933 | represented by a dotted-quad string, i.e. '1.2.3.4'. 934 | 935 | >>> vtor.check('ip_addr', '1 ') 936 | '1' 937 | >>> vtor.check('ip_addr', ' 1.2') 938 | '1.2' 939 | >>> vtor.check('ip_addr', ' 1.2.3 ') 940 | '1.2.3' 941 | >>> vtor.check('ip_addr', '1.2.3.4') 942 | '1.2.3.4' 943 | >>> vtor.check('ip_addr', '0.0.0.0') 944 | '0.0.0.0' 945 | >>> vtor.check('ip_addr', '255.255.255.255') 946 | '255.255.255.255' 947 | >>> vtor.check('ip_addr', '255.255.255.256') 948 | Traceback (most recent call last): 949 | VdtValueError: the value "255.255.255.256" is unacceptable. 950 | >>> vtor.check('ip_addr', '1.2.3.4.5') 951 | Traceback (most recent call last): 952 | VdtValueError: the value "1.2.3.4.5" is unacceptable. 953 | >>> vtor.check('ip_addr', 0) 954 | Traceback (most recent call last): 955 | VdtTypeError: the value "0" is of the wrong type. 956 | """ 957 | if not isinstance(value, str): 958 | raise VdtTypeError(value) 959 | value = value.strip() 960 | try: 961 | dottedQuadToNum(value) 962 | except ValueError: 963 | raise VdtValueError(value) 964 | return value 965 | 966 | 967 | def is_list(value, min=None, max=None): 968 | """ 969 | Check that the value is a list of values. 970 | 971 | You can optionally specify the minimum and maximum number of members. 972 | 973 | It does no check on list members. 974 | 975 | >>> vtor.check('list', ()) 976 | [] 977 | >>> vtor.check('list', []) 978 | [] 979 | >>> vtor.check('list', (1, 2)) 980 | [1, 2] 981 | >>> vtor.check('list', [1, 2]) 982 | [1, 2] 983 | >>> vtor.check('list(3)', (1, 2)) 984 | Traceback (most recent call last): 985 | VdtValueTooShortError: the value "(1, 2)" is too short. 986 | >>> vtor.check('list(max=5)', (1, 2, 3, 4, 5, 6)) 987 | Traceback (most recent call last): 988 | VdtValueTooLongError: the value "(1, 2, 3, 4, 5, 6)" is too long. 989 | >>> vtor.check('list(min=3, max=5)', (1, 2, 3, 4)) 990 | [1, 2, 3, 4] 991 | >>> vtor.check('list', 0) 992 | Traceback (most recent call last): 993 | VdtTypeError: the value "0" is of the wrong type. 994 | >>> vtor.check('list', '12') 995 | Traceback (most recent call last): 996 | VdtTypeError: the value "12" is of the wrong type. 997 | """ 998 | (min_len, max_len) = _is_num_param(('min', 'max'), (min, max)) 999 | if isinstance(value, str): 1000 | raise VdtTypeError(value) 1001 | try: 1002 | num_members = len(value) 1003 | except TypeError: 1004 | raise VdtTypeError(value) 1005 | if min_len is not None and num_members < min_len: 1006 | raise VdtValueTooShortError(value) 1007 | if max_len is not None and num_members > max_len: 1008 | raise VdtValueTooLongError(value) 1009 | return list(value) 1010 | 1011 | 1012 | def is_tuple(value, min=None, max=None): 1013 | """ 1014 | Check that the value is a tuple of values. 1015 | 1016 | You can optionally specify the minimum and maximum number of members. 1017 | 1018 | It does no check on members. 1019 | 1020 | >>> vtor.check('tuple', ()) 1021 | () 1022 | >>> vtor.check('tuple', []) 1023 | () 1024 | >>> vtor.check('tuple', (1, 2)) 1025 | (1, 2) 1026 | >>> vtor.check('tuple', [1, 2]) 1027 | (1, 2) 1028 | >>> vtor.check('tuple(3)', (1, 2)) 1029 | Traceback (most recent call last): 1030 | VdtValueTooShortError: the value "(1, 2)" is too short. 1031 | >>> vtor.check('tuple(max=5)', (1, 2, 3, 4, 5, 6)) 1032 | Traceback (most recent call last): 1033 | VdtValueTooLongError: the value "(1, 2, 3, 4, 5, 6)" is too long. 1034 | >>> vtor.check('tuple(min=3, max=5)', (1, 2, 3, 4)) 1035 | (1, 2, 3, 4) 1036 | >>> vtor.check('tuple', 0) 1037 | Traceback (most recent call last): 1038 | VdtTypeError: the value "0" is of the wrong type. 1039 | >>> vtor.check('tuple', '12') 1040 | Traceback (most recent call last): 1041 | VdtTypeError: the value "12" is of the wrong type. 1042 | """ 1043 | return tuple(is_list(value, min, max)) 1044 | 1045 | 1046 | def is_string(value, min=None, max=None): 1047 | """ 1048 | Check that the supplied value is a string. 1049 | 1050 | You can optionally specify the minimum and maximum number of members. 1051 | 1052 | >>> vtor.check('string', '0') 1053 | '0' 1054 | >>> vtor.check('string', 0) 1055 | Traceback (most recent call last): 1056 | VdtTypeError: the value "0" is of the wrong type. 1057 | >>> vtor.check('string(2)', '12') 1058 | '12' 1059 | >>> vtor.check('string(2)', '1') 1060 | Traceback (most recent call last): 1061 | VdtValueTooShortError: the value "1" is too short. 1062 | >>> vtor.check('string(min=2, max=3)', '123') 1063 | '123' 1064 | >>> vtor.check('string(min=2, max=3)', '1234') 1065 | Traceback (most recent call last): 1066 | VdtValueTooLongError: the value "1234" is too long. 1067 | """ 1068 | if not isinstance(value, str): 1069 | raise VdtTypeError(value) 1070 | (min_len, max_len) = _is_num_param(('min', 'max'), (min, max)) 1071 | try: 1072 | num_members = len(value) 1073 | except TypeError: 1074 | raise VdtTypeError(value) 1075 | if min_len is not None and num_members < min_len: 1076 | raise VdtValueTooShortError(value) 1077 | if max_len is not None and num_members > max_len: 1078 | raise VdtValueTooLongError(value) 1079 | return value 1080 | 1081 | 1082 | def is_int_list(value, min=None, max=None): 1083 | """ 1084 | Check that the value is a list of integers. 1085 | 1086 | You can optionally specify the minimum and maximum number of members. 1087 | 1088 | Each list member is checked that it is an integer. 1089 | 1090 | >>> vtor.check('int_list', ()) 1091 | [] 1092 | >>> vtor.check('int_list', []) 1093 | [] 1094 | >>> vtor.check('int_list', (1, 2)) 1095 | [1, 2] 1096 | >>> vtor.check('int_list', [1, 2]) 1097 | [1, 2] 1098 | >>> vtor.check('int_list', [1, 'a']) 1099 | Traceback (most recent call last): 1100 | VdtTypeError: the value "a" is of the wrong type. 1101 | """ 1102 | return [is_integer(mem) for mem in is_list(value, min, max)] 1103 | 1104 | 1105 | def is_bool_list(value, min=None, max=None): 1106 | """ 1107 | Check that the value is a list of booleans. 1108 | 1109 | You can optionally specify the minimum and maximum number of members. 1110 | 1111 | Each list member is checked that it is a boolean. 1112 | 1113 | >>> vtor.check('bool_list', ()) 1114 | [] 1115 | >>> vtor.check('bool_list', []) 1116 | [] 1117 | >>> check_res = vtor.check('bool_list', (True, False)) 1118 | >>> check_res == [True, False] 1119 | 1 1120 | >>> check_res = vtor.check('bool_list', [True, False]) 1121 | >>> check_res == [True, False] 1122 | 1 1123 | >>> vtor.check('bool_list', [True, 'a']) 1124 | Traceback (most recent call last): 1125 | VdtTypeError: the value "a" is of the wrong type. 1126 | """ 1127 | return [is_boolean(mem) for mem in is_list(value, min, max)] 1128 | 1129 | 1130 | def is_float_list(value, min=None, max=None): 1131 | """ 1132 | Check that the value is a list of floats. 1133 | 1134 | You can optionally specify the minimum and maximum number of members. 1135 | 1136 | Each list member is checked that it is a float. 1137 | 1138 | >>> vtor.check('float_list', ()) 1139 | [] 1140 | >>> vtor.check('float_list', []) 1141 | [] 1142 | >>> vtor.check('float_list', (1, 2.0)) 1143 | [1.0, 2.0] 1144 | >>> vtor.check('float_list', [1, 2.0]) 1145 | [1.0, 2.0] 1146 | >>> vtor.check('float_list', [1, 'a']) 1147 | Traceback (most recent call last): 1148 | VdtTypeError: the value "a" is of the wrong type. 1149 | """ 1150 | return [is_float(mem) for mem in is_list(value, min, max)] 1151 | 1152 | 1153 | def is_string_list(value, min=None, max=None): 1154 | """ 1155 | Check that the value is a list of strings. 1156 | 1157 | You can optionally specify the minimum and maximum number of members. 1158 | 1159 | Each list member is checked that it is a string. 1160 | 1161 | >>> vtor.check('string_list', ()) 1162 | [] 1163 | >>> vtor.check('string_list', []) 1164 | [] 1165 | >>> vtor.check('string_list', ('a', 'b')) 1166 | ['a', 'b'] 1167 | >>> vtor.check('string_list', ['a', 1]) 1168 | Traceback (most recent call last): 1169 | VdtTypeError: the value "1" is of the wrong type. 1170 | >>> vtor.check('string_list', 'hello') 1171 | Traceback (most recent call last): 1172 | VdtTypeError: the value "hello" is of the wrong type. 1173 | """ 1174 | if isinstance(value, str): 1175 | raise VdtTypeError(value) 1176 | return [is_string(mem) for mem in is_list(value, min, max)] 1177 | 1178 | 1179 | def is_ip_addr_list(value, min=None, max=None): 1180 | """ 1181 | Check that the value is a list of IP addresses. 1182 | 1183 | You can optionally specify the minimum and maximum number of members. 1184 | 1185 | Each list member is checked that it is an IP address. 1186 | 1187 | >>> vtor.check('ip_addr_list', ()) 1188 | [] 1189 | >>> vtor.check('ip_addr_list', []) 1190 | [] 1191 | >>> vtor.check('ip_addr_list', ('1.2.3.4', '5.6.7.8')) 1192 | ['1.2.3.4', '5.6.7.8'] 1193 | >>> vtor.check('ip_addr_list', ['a']) 1194 | Traceback (most recent call last): 1195 | VdtValueError: the value "a" is unacceptable. 1196 | """ 1197 | return [is_ip_addr(mem) for mem in is_list(value, min, max)] 1198 | 1199 | 1200 | def force_list(value, min=None, max=None): 1201 | """ 1202 | Check that a value is a list, coercing strings into 1203 | a list with one member. Useful where users forget the 1204 | trailing comma that turns a single value into a list. 1205 | 1206 | You can optionally specify the minimum and maximum number of members. 1207 | A minumum of greater than one will fail if the user only supplies a 1208 | string. 1209 | 1210 | >>> vtor.check('force_list', ()) 1211 | [] 1212 | >>> vtor.check('force_list', []) 1213 | [] 1214 | >>> vtor.check('force_list', 'hello') 1215 | ['hello'] 1216 | """ 1217 | if not isinstance(value, (list, tuple)): 1218 | value = [value] 1219 | return is_list(value, min, max) 1220 | 1221 | 1222 | 1223 | fun_dict = { 1224 | 'integer': is_integer, 1225 | 'float': is_float, 1226 | 'ip_addr': is_ip_addr, 1227 | 'string': is_string, 1228 | 'boolean': is_boolean, 1229 | } 1230 | 1231 | 1232 | def is_mixed_list(value, *args): 1233 | """ 1234 | Check that the value is a list. 1235 | Allow specifying the type of each member. 1236 | Work on lists of specific lengths. 1237 | 1238 | You specify each member as a positional argument specifying type 1239 | 1240 | Each type should be one of the following strings : 1241 | 'integer', 'float', 'ip_addr', 'string', 'boolean' 1242 | 1243 | So you can specify a list of two strings, followed by 1244 | two integers as : 1245 | 1246 | mixed_list('string', 'string', 'integer', 'integer') 1247 | 1248 | The length of the list must match the number of positional 1249 | arguments you supply. 1250 | 1251 | >>> mix_str = "mixed_list('integer', 'float', 'ip_addr', 'string', 'boolean')" 1252 | >>> check_res = vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a', True)) 1253 | >>> check_res == [1, 2.0, '1.2.3.4', 'a', True] 1254 | 1 1255 | >>> check_res = vtor.check(mix_str, ('1', '2.0', '1.2.3.4', 'a', 'True')) 1256 | >>> check_res == [1, 2.0, '1.2.3.4', 'a', True] 1257 | 1 1258 | >>> vtor.check(mix_str, ('b', 2.0, '1.2.3.4', 'a', True)) 1259 | Traceback (most recent call last): 1260 | VdtTypeError: the value "b" is of the wrong type. 1261 | >>> vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a')) 1262 | Traceback (most recent call last): 1263 | VdtValueTooShortError: the value "(1, 2.0, '1.2.3.4', 'a')" is too short. 1264 | >>> vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a', 1, 'b')) 1265 | Traceback (most recent call last): 1266 | VdtValueTooLongError: the value "(1, 2.0, '1.2.3.4', 'a', 1, 'b')" is too long. 1267 | >>> vtor.check(mix_str, 0) 1268 | Traceback (most recent call last): 1269 | VdtTypeError: the value "0" is of the wrong type. 1270 | 1271 | >>> vtor.check('mixed_list("yoda")', ('a')) 1272 | Traceback (most recent call last): 1273 | VdtParamError: passed an incorrect value "KeyError('yoda',)" for parameter "'mixed_list'" 1274 | """ 1275 | try: 1276 | length = len(value) 1277 | except TypeError: 1278 | raise VdtTypeError(value) 1279 | if length < len(args): 1280 | raise VdtValueTooShortError(value) 1281 | elif length > len(args): 1282 | raise VdtValueTooLongError(value) 1283 | try: 1284 | return [fun_dict[arg](val) for arg, val in zip(args, value)] 1285 | except KeyError as e: 1286 | raise VdtParamError('mixed_list', e) 1287 | 1288 | 1289 | def is_option(value, *options): 1290 | """ 1291 | This check matches the value to any of a set of options. 1292 | 1293 | >>> vtor.check('option("yoda", "jedi")', 'yoda') 1294 | 'yoda' 1295 | >>> vtor.check('option("yoda", "jedi")', 'jed') 1296 | Traceback (most recent call last): 1297 | VdtValueError: the value "jed" is unacceptable. 1298 | >>> vtor.check('option("yoda", "jedi")', 0) 1299 | Traceback (most recent call last): 1300 | VdtTypeError: the value "0" is of the wrong type. 1301 | """ 1302 | if not isinstance(value, str): 1303 | raise VdtTypeError(value) 1304 | if not value in options: 1305 | raise VdtValueError(value) 1306 | return value 1307 | 1308 | 1309 | def _test(value, *args, **keywargs): 1310 | """ 1311 | A function that exists for test purposes. 1312 | 1313 | >>> checks = [ 1314 | ... '3, 6, min=1, max=3, test=list(a, b, c)', 1315 | ... '3', 1316 | ... '3, 6', 1317 | ... '3,', 1318 | ... 'min=1, test="a b c"', 1319 | ... 'min=5, test="a, b, c"', 1320 | ... 'min=1, max=3, test="a, b, c"', 1321 | ... 'min=-100, test=-99', 1322 | ... 'min=1, max=3', 1323 | ... '3, 6, test="36"', 1324 | ... '3, 6, test="a, b, c"', 1325 | ... '3, max=3, test=list("a", "b", "c")', 1326 | ... '''3, max=3, test=list("'a'", 'b', "x=(c)")''', 1327 | ... "test='x=fish(3)'", 1328 | ... ] 1329 | >>> v = Validator({'test': _test}) 1330 | >>> for entry in checks: 1331 | ... pprint(v.check(('test(%s)' % entry), 3)) 1332 | (3, ('3', '6'), {'max': '3', 'min': '1', 'test': ['a', 'b', 'c']}) 1333 | (3, ('3',), {}) 1334 | (3, ('3', '6'), {}) 1335 | (3, ('3',), {}) 1336 | (3, (), {'min': '1', 'test': 'a b c'}) 1337 | (3, (), {'min': '5', 'test': 'a, b, c'}) 1338 | (3, (), {'max': '3', 'min': '1', 'test': 'a, b, c'}) 1339 | (3, (), {'min': '-100', 'test': '-99'}) 1340 | (3, (), {'max': '3', 'min': '1'}) 1341 | (3, ('3', '6'), {'test': '36'}) 1342 | (3, ('3', '6'), {'test': 'a, b, c'}) 1343 | (3, ('3',), {'max': '3', 'test': ['a', 'b', 'c']}) 1344 | (3, ('3',), {'max': '3', 'test': ["'a'", 'b', 'x=(c)']}) 1345 | (3, (), {'test': 'x=fish(3)'}) 1346 | 1347 | >>> v = Validator() 1348 | >>> v.check('integer(default=6)', '3') 1349 | 3 1350 | >>> v.check('integer(default=6)', None, True) 1351 | 6 1352 | >>> v.get_default_value('integer(default=6)') 1353 | 6 1354 | >>> v.get_default_value('float(default=6)') 1355 | 6.0 1356 | >>> v.get_default_value('pass(default=None)') 1357 | >>> v.get_default_value("string(default='None')") 1358 | 'None' 1359 | >>> v.get_default_value('pass') 1360 | Traceback (most recent call last): 1361 | KeyError: 'Check "pass" has no default value.' 1362 | >>> v.get_default_value('pass(default=list(1, 2, 3, 4))') 1363 | ['1', '2', '3', '4'] 1364 | 1365 | >>> v = Validator() 1366 | >>> v.check("pass(default=None)", None, True) 1367 | >>> v.check("pass(default='None')", None, True) 1368 | 'None' 1369 | >>> v.check('pass(default="None")', None, True) 1370 | 'None' 1371 | >>> v.check('pass(default=list(1, 2, 3, 4))', None, True) 1372 | ['1', '2', '3', '4'] 1373 | 1374 | Bug test for unicode arguments 1375 | >>> v = Validator() 1376 | >>> v.check('string(min=4)', 'test') == 'test' 1377 | True 1378 | 1379 | >>> v = Validator() 1380 | >>> v.get_default_value('string(min=4, default="1234")') == '1234' 1381 | True 1382 | >>> v.check('string(min=4, default="1234")', 'test') == 'test' 1383 | True 1384 | 1385 | >>> v = Validator() 1386 | >>> default = v.get_default_value('string(default=None)') 1387 | >>> default == None 1388 | 1 1389 | """ 1390 | return (value, args, keywargs) 1391 | 1392 | 1393 | def _test2(): 1394 | """ 1395 | >>> 1396 | >>> v = Validator() 1397 | >>> v.get_default_value('string(default="#ff00dd")') 1398 | '#ff00dd' 1399 | >>> v.get_default_value('integer(default=3) # comment') 1400 | 3 1401 | """ 1402 | 1403 | def _test3(): 1404 | r""" 1405 | >>> vtor.check('string(default="")', '', missing=True) 1406 | '' 1407 | >>> vtor.check('string(default="\n")', '', missing=True) 1408 | '\n' 1409 | >>> print(vtor.check('string(default="\n")', '', missing=True)) 1410 | 1411 | 1412 | >>> vtor.check('string()', '\n') 1413 | '\n' 1414 | >>> vtor.check('string(default="\n\n\n")', '', missing=True) 1415 | '\n\n\n' 1416 | >>> vtor.check('string()', 'random \n text goes here\n\n') 1417 | 'random \n text goes here\n\n' 1418 | >>> vtor.check('string(default=" \nrandom text\ngoes \n here\n\n ")', 1419 | ... '', missing=True) 1420 | ' \nrandom text\ngoes \n here\n\n ' 1421 | >>> vtor.check("string(default='\n\n\n')", '', missing=True) 1422 | '\n\n\n' 1423 | >>> vtor.check("option('\n','a','b',default='\n')", '', missing=True) 1424 | '\n' 1425 | >>> vtor.check("string_list()", ['foo', '\n', 'bar']) 1426 | ['foo', '\n', 'bar'] 1427 | >>> vtor.check("string_list(default=list('\n'))", '', missing=True) 1428 | ['\n'] 1429 | """ 1430 | 1431 | 1432 | if __name__ == '__main__': 1433 | # run the code tests in doctest format 1434 | import sys 1435 | import doctest 1436 | m = sys.modules.get('__main__') 1437 | globs = m.__dict__.copy() 1438 | globs.update({ 1439 | 'vtor': Validator(), 1440 | }) 1441 | 1442 | failures, tests = doctest.testmod( 1443 | m, globs=globs, 1444 | optionflags=doctest.IGNORE_EXCEPTION_DETAIL | doctest.ELLIPSIS) 1445 | assert not failures, '{} failures out of {} tests'.format(failures, tests) 1446 | -------------------------------------------------------------------------------- /src/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiffSK/configobj/b7707c94c0317b8f89b704a01525a68eb6c72521/src/tests/__init__.py -------------------------------------------------------------------------------- /src/tests/conf.ini: -------------------------------------------------------------------------------- 1 | 2 | extra = 3 3 | 4 | [extra-section] 5 | 6 | [section] 7 | [[sub-section]] 8 | extra = 3 9 | [[extra-sub-section]] 10 | extra = 3 11 | -------------------------------------------------------------------------------- /src/tests/conf.spec: -------------------------------------------------------------------------------- 1 | 2 | value = integer 3 | 4 | [section] 5 | value = integer 6 | 7 | [[sub-section]] 8 | value = integer 9 | [[missing-subsection]] 10 | value = integer 11 | 12 | [missing-section] 13 | value = integer 14 | -------------------------------------------------------------------------------- /src/tests/configobj_doctests.py: -------------------------------------------------------------------------------- 1 | # configobj_test.py 2 | # doctests for ConfigObj 3 | # A config file reader/writer that supports nested sections in config files. 4 | # Copyright (C) 2005-2014: 5 | # (name) : (email) 6 | # Michael Foord: fuzzyman AT voidspace DOT org DOT uk 7 | # Nicola Larosa: nico AT tekNico DOT net 8 | # Rob Dennis: rdennis AT gmail DOT com 9 | # Eli Courtwright: eli AT courtwright DOT org 10 | 11 | # This software is licensed under the terms of the BSD license. 12 | # http://opensource.org/licenses/BSD-3-Clause 13 | 14 | # ConfigObj 5 - main repository for documentation and issue tracking: 15 | # https://github.com/DiffSK/configobj 16 | 17 | import sys 18 | 19 | from io import StringIO 20 | 21 | import sys 22 | 23 | from configobj import * 24 | from configobj.validate import Validator 25 | 26 | 27 | def _test_validate(): 28 | """ 29 | >>> val = Validator() 30 | 31 | >>> a = ['foo = fish'] 32 | >>> b = ['foo = integer(default=3)'] 33 | >>> c = ConfigObj(a, configspec=b) 34 | >>> c 35 | ConfigObj({'foo': 'fish'}) 36 | >>> from configobj.validate import Validator 37 | >>> v = Validator() 38 | >>> c.validate(v) 39 | 0 40 | >>> c.default_values 41 | {'foo': 3} 42 | >>> c.restore_default('foo') 43 | 3 44 | 45 | Now testing with repeated sections : BIG TEST 46 | 47 | >>> repeated_1 = ''' 48 | ... [dogs] 49 | ... [[__many__]] # spec for a dog 50 | ... fleas = boolean(default=True) 51 | ... tail = option(long, short, default=long) 52 | ... name = string(default=rover) 53 | ... [[[__many__]]] # spec for a puppy 54 | ... name = string(default="son of rover") 55 | ... age = float(default=0.0) 56 | ... [cats] 57 | ... [[__many__]] # spec for a cat 58 | ... fleas = boolean(default=True) 59 | ... tail = option(long, short, default=short) 60 | ... name = string(default=pussy) 61 | ... [[[__many__]]] # spec for a kitten 62 | ... name = string(default="son of pussy") 63 | ... age = float(default=0.0) 64 | ... '''.split('\\n') 65 | >>> repeated_2 = ''' 66 | ... [dogs] 67 | ... 68 | ... # blank dogs with puppies 69 | ... # should be filled in by the configspec 70 | ... [[dog1]] 71 | ... [[[puppy1]]] 72 | ... [[[puppy2]]] 73 | ... [[[puppy3]]] 74 | ... [[dog2]] 75 | ... [[[puppy1]]] 76 | ... [[[puppy2]]] 77 | ... [[[puppy3]]] 78 | ... [[dog3]] 79 | ... [[[puppy1]]] 80 | ... [[[puppy2]]] 81 | ... [[[puppy3]]] 82 | ... [cats] 83 | ... 84 | ... # blank cats with kittens 85 | ... # should be filled in by the configspec 86 | ... [[cat1]] 87 | ... [[[kitten1]]] 88 | ... [[[kitten2]]] 89 | ... [[[kitten3]]] 90 | ... [[cat2]] 91 | ... [[[kitten1]]] 92 | ... [[[kitten2]]] 93 | ... [[[kitten3]]] 94 | ... [[cat3]] 95 | ... [[[kitten1]]] 96 | ... [[[kitten2]]] 97 | ... [[[kitten3]]] 98 | ... '''.split('\\n') 99 | >>> repeated_3 = ''' 100 | ... [dogs] 101 | ... 102 | ... [[dog1]] 103 | ... [[dog2]] 104 | ... [[dog3]] 105 | ... [cats] 106 | ... 107 | ... [[cat1]] 108 | ... [[cat2]] 109 | ... [[cat3]] 110 | ... '''.split('\\n') 111 | >>> repeated_4 = ''' 112 | ... [__many__] 113 | ... 114 | ... name = string(default=Michael) 115 | ... age = float(default=0.0) 116 | ... sex = option(m, f, default=m) 117 | ... '''.split('\\n') 118 | >>> repeated_5 = ''' 119 | ... [cats] 120 | ... [[__many__]] 121 | ... fleas = boolean(default=True) 122 | ... tail = option(long, short, default=short) 123 | ... name = string(default=pussy) 124 | ... [[[description]]] 125 | ... height = float(default=3.3) 126 | ... weight = float(default=6) 127 | ... [[[[coat]]]] 128 | ... fur = option(black, grey, brown, "tortoise shell", default=black) 129 | ... condition = integer(0,10, default=5) 130 | ... '''.split('\\n') 131 | >>> val= Validator() 132 | >>> repeater = ConfigObj(repeated_2, configspec=repeated_1) 133 | >>> repeater.validate(val) 134 | 1 135 | >>> repeater == { 136 | ... 'dogs': { 137 | ... 'dog1': { 138 | ... 'fleas': True, 139 | ... 'tail': 'long', 140 | ... 'name': 'rover', 141 | ... 'puppy1': {'name': 'son of rover', 'age': 0.0}, 142 | ... 'puppy2': {'name': 'son of rover', 'age': 0.0}, 143 | ... 'puppy3': {'name': 'son of rover', 'age': 0.0}, 144 | ... }, 145 | ... 'dog2': { 146 | ... 'fleas': True, 147 | ... 'tail': 'long', 148 | ... 'name': 'rover', 149 | ... 'puppy1': {'name': 'son of rover', 'age': 0.0}, 150 | ... 'puppy2': {'name': 'son of rover', 'age': 0.0}, 151 | ... 'puppy3': {'name': 'son of rover', 'age': 0.0}, 152 | ... }, 153 | ... 'dog3': { 154 | ... 'fleas': True, 155 | ... 'tail': 'long', 156 | ... 'name': 'rover', 157 | ... 'puppy1': {'name': 'son of rover', 'age': 0.0}, 158 | ... 'puppy2': {'name': 'son of rover', 'age': 0.0}, 159 | ... 'puppy3': {'name': 'son of rover', 'age': 0.0}, 160 | ... }, 161 | ... }, 162 | ... 'cats': { 163 | ... 'cat1': { 164 | ... 'fleas': True, 165 | ... 'tail': 'short', 166 | ... 'name': 'pussy', 167 | ... 'kitten1': {'name': 'son of pussy', 'age': 0.0}, 168 | ... 'kitten2': {'name': 'son of pussy', 'age': 0.0}, 169 | ... 'kitten3': {'name': 'son of pussy', 'age': 0.0}, 170 | ... }, 171 | ... 'cat2': { 172 | ... 'fleas': True, 173 | ... 'tail': 'short', 174 | ... 'name': 'pussy', 175 | ... 'kitten1': {'name': 'son of pussy', 'age': 0.0}, 176 | ... 'kitten2': {'name': 'son of pussy', 'age': 0.0}, 177 | ... 'kitten3': {'name': 'son of pussy', 'age': 0.0}, 178 | ... }, 179 | ... 'cat3': { 180 | ... 'fleas': True, 181 | ... 'tail': 'short', 182 | ... 'name': 'pussy', 183 | ... 'kitten1': {'name': 'son of pussy', 'age': 0.0}, 184 | ... 'kitten2': {'name': 'son of pussy', 'age': 0.0}, 185 | ... 'kitten3': {'name': 'son of pussy', 'age': 0.0}, 186 | ... }, 187 | ... }, 188 | ... } 189 | 1 190 | >>> repeater = ConfigObj(repeated_3, configspec=repeated_1) 191 | >>> repeater.validate(val) 192 | 1 193 | >>> repeater == { 194 | ... 'cats': { 195 | ... 'cat1': {'fleas': True, 'tail': 'short', 'name': 'pussy'}, 196 | ... 'cat2': {'fleas': True, 'tail': 'short', 'name': 'pussy'}, 197 | ... 'cat3': {'fleas': True, 'tail': 'short', 'name': 'pussy'}, 198 | ... }, 199 | ... 'dogs': { 200 | ... 'dog1': {'fleas': True, 'tail': 'long', 'name': 'rover'}, 201 | ... 'dog2': {'fleas': True, 'tail': 'long', 'name': 'rover'}, 202 | ... 'dog3': {'fleas': True, 'tail': 'long', 'name': 'rover'}, 203 | ... }, 204 | ... } 205 | 1 206 | >>> repeater = ConfigObj(configspec=repeated_4) 207 | >>> repeater['Michael'] = {} 208 | >>> repeater.validate(val) 209 | 1 210 | >>> repeater == { 211 | ... 'Michael': {'age': 0.0, 'name': 'Michael', 'sex': 'm'}, 212 | ... } 213 | 1 214 | >>> repeater = ConfigObj(repeated_3, configspec=repeated_5) 215 | >>> repeater == { 216 | ... 'dogs': {'dog1': {}, 'dog2': {}, 'dog3': {}}, 217 | ... 'cats': {'cat1': {}, 'cat2': {}, 'cat3': {}}, 218 | ... } 219 | 1 220 | >>> repeater.validate(val) 221 | 1 222 | >>> repeater == { 223 | ... 'dogs': {'dog1': {}, 'dog2': {}, 'dog3': {}}, 224 | ... 'cats': { 225 | ... 'cat1': { 226 | ... 'fleas': True, 227 | ... 'tail': 'short', 228 | ... 'name': 'pussy', 229 | ... 'description': { 230 | ... 'weight': 6.0, 231 | ... 'height': 3.2999999999999998, 232 | ... 'coat': {'fur': 'black', 'condition': 5}, 233 | ... }, 234 | ... }, 235 | ... 'cat2': { 236 | ... 'fleas': True, 237 | ... 'tail': 'short', 238 | ... 'name': 'pussy', 239 | ... 'description': { 240 | ... 'weight': 6.0, 241 | ... 'height': 3.2999999999999998, 242 | ... 'coat': {'fur': 'black', 'condition': 5}, 243 | ... }, 244 | ... }, 245 | ... 'cat3': { 246 | ... 'fleas': True, 247 | ... 'tail': 'short', 248 | ... 'name': 'pussy', 249 | ... 'description': { 250 | ... 'weight': 6.0, 251 | ... 'height': 3.2999999999999998, 252 | ... 'coat': {'fur': 'black', 'condition': 5}, 253 | ... }, 254 | ... }, 255 | ... }, 256 | ... } 257 | 1 258 | 259 | Test that interpolation is preserved for validated string values. 260 | Also check that interpolation works in configspecs. 261 | >>> t = ConfigObj(configspec=['test = string']) 262 | >>> t['DEFAULT'] = {} 263 | >>> t['DEFAULT']['def_test'] = 'a' 264 | >>> t['test'] = '%(def_test)s' 265 | >>> t['test'] 266 | 'a' 267 | >>> v = Validator() 268 | >>> t.validate(v) 269 | 1 270 | >>> t.interpolation = False 271 | >>> t 272 | ConfigObj({'test': '%(def_test)s', 'DEFAULT': {'def_test': 'a'}}) 273 | >>> specs = [ 274 | ... 'interpolated string = string(default="fuzzy-%(man)s")', 275 | ... '[DEFAULT]', 276 | ... 'man = wuzzy', 277 | ... ] 278 | >>> c = ConfigObj(configspec=specs) 279 | >>> c.validate(v) 280 | 1 281 | >>> c['interpolated string'] 282 | 'fuzzy-wuzzy' 283 | 284 | Test SimpleVal 285 | >>> val = SimpleVal() 286 | >>> config = ''' 287 | ... test1=40 288 | ... test2=hello 289 | ... test3=3 290 | ... test4=5.0 291 | ... [section] 292 | ... test1=40 293 | ... test2=hello 294 | ... test3=3 295 | ... test4=5.0 296 | ... [[sub section]] 297 | ... test1=40 298 | ... test2=hello 299 | ... test3=3 300 | ... test4=5.0 301 | ... '''.split('\\n') 302 | >>> configspec = ''' 303 | ... test1='' 304 | ... test2='' 305 | ... test3='' 306 | ... test4='' 307 | ... [section] 308 | ... test1='' 309 | ... test2='' 310 | ... test3='' 311 | ... test4='' 312 | ... [[sub section]] 313 | ... test1='' 314 | ... test2='' 315 | ... test3='' 316 | ... test4='' 317 | ... '''.split('\\n') 318 | >>> o = ConfigObj(config, configspec=configspec) 319 | >>> o.validate(val) 320 | 1 321 | >>> o = ConfigObj(configspec=configspec) 322 | >>> o.validate(val) 323 | 0 324 | 325 | Test Flatten Errors 326 | >>> vtor = Validator() 327 | >>> my_ini = ''' 328 | ... option1 = True 329 | ... [section1] 330 | ... option1 = True 331 | ... [section2] 332 | ... another_option = Probably 333 | ... [section3] 334 | ... another_option = True 335 | ... [[section3b]] 336 | ... value = 3 337 | ... value2 = a 338 | ... value3 = 11 339 | ... ''' 340 | >>> my_cfg = ''' 341 | ... option1 = boolean() 342 | ... option2 = boolean() 343 | ... option3 = boolean(default=Bad_value) 344 | ... [section1] 345 | ... option1 = boolean() 346 | ... option2 = boolean() 347 | ... option3 = boolean(default=Bad_value) 348 | ... [section2] 349 | ... another_option = boolean() 350 | ... [section3] 351 | ... another_option = boolean() 352 | ... [[section3b]] 353 | ... value = integer 354 | ... value2 = integer 355 | ... value3 = integer(0, 10) 356 | ... [[[section3b-sub]]] 357 | ... value = string 358 | ... [section4] 359 | ... another_option = boolean() 360 | ... ''' 361 | >>> cs = my_cfg.split('\\n') 362 | >>> ini = my_ini.split('\\n') 363 | >>> cfg = ConfigObj(ini, configspec=cs) 364 | >>> res = cfg.validate(vtor, preserve_errors=True) 365 | >>> errors = [] 366 | >>> for entry in flatten_errors(cfg, res): 367 | ... section_list, key, error = entry 368 | ... section_list.insert(0, '[root]') 369 | ... if key is not None: 370 | ... section_list.append(key) 371 | ... section_string = ', '.join(section_list) 372 | ... errors.append('%s%s%s' % (section_string, ' = ', error or 'missing')) 373 | >>> errors.sort() 374 | >>> for entry in errors: 375 | ... print(entry) 376 | [root], option2 = missing 377 | [root], option3 = the value "Bad_value" is of the wrong type. 378 | [root], section1, option2 = missing 379 | [root], section1, option3 = the value "Bad_value" is of the wrong type. 380 | [root], section2, another_option = the value "Probably" is of the wrong type. 381 | [root], section3, section3b, section3b-sub = missing 382 | [root], section3, section3b, value2 = the value "a" is of the wrong type. 383 | [root], section3, section3b, value3 = the value "11" is too big. 384 | [root], section4 = missing 385 | """ 386 | 387 | 388 | def _test_errors(): 389 | """ 390 | Test the error messages and objects, in normal mode and unrepr mode. 391 | >>> bad_syntax = ''' 392 | ... key = "value" 393 | ... key2 = "value 394 | ... '''.splitlines() 395 | >>> c = ConfigObj(bad_syntax) 396 | Traceback (most recent call last): 397 | ParseError: Parse error in value at line 3. 398 | >>> c = ConfigObj(bad_syntax, raise_errors=True) 399 | Traceback (most recent call last): 400 | ParseError: Parse error in value at line 3. 401 | >>> c = ConfigObj(bad_syntax, raise_errors=True, unrepr=True) 402 | Traceback (most recent call last): 403 | UnreprError: Parse error in value at line 3. 404 | >>> try: 405 | ... c = ConfigObj(bad_syntax) 406 | ... except Exception as exc: 407 | ... e = exc 408 | >>> assert(isinstance(e, ConfigObjError)) 409 | >>> print(e) 410 | Parse error in value at line 3. 411 | >>> len(e.errors) == 1 412 | 1 413 | >>> try: 414 | ... c = ConfigObj(bad_syntax, unrepr=True) 415 | ... except Exception as exc: 416 | ... e = exc 417 | >>> assert(isinstance(e, ConfigObjError)) 418 | >>> print(e) 419 | Parse error from unrepr-ing value at line 3. 420 | >>> len(e.errors) == 1 421 | 1 422 | >>> the_error = e.errors[0] 423 | >>> assert(isinstance(the_error, UnreprError)) 424 | 425 | >>> multiple_bad_syntax = ''' 426 | ... key = "value" 427 | ... key2 = "value 428 | ... key3 = "value2 429 | ... '''.splitlines() 430 | >>> try: 431 | ... c = ConfigObj(multiple_bad_syntax) 432 | ... except ConfigObjError as e: 433 | ... str(e) 434 | 'Parsing failed with several errors.\\nFirst error at line 3.' 435 | >>> c = ConfigObj(multiple_bad_syntax, raise_errors=True) 436 | Traceback (most recent call last): 437 | ParseError: Parse error in value at line 3. 438 | >>> c = ConfigObj(multiple_bad_syntax, raise_errors=True, unrepr=True) 439 | Traceback (most recent call last): 440 | UnreprError: Parse error in value at line 3. 441 | >>> try: 442 | ... c = ConfigObj(multiple_bad_syntax) 443 | ... except Exception as exc: 444 | ... e = exc 445 | >>> assert(isinstance(e, ConfigObjError)) 446 | >>> print(e) 447 | Parsing failed with several errors. 448 | First error at line 3. 449 | >>> len(e.errors) == 2 450 | 1 451 | >>> try: 452 | ... c = ConfigObj(multiple_bad_syntax, unrepr=True) 453 | ... except Exception as exc: 454 | ... e = exc 455 | >>> assert(isinstance(e, ConfigObjError)) 456 | >>> print(e) 457 | Parsing failed with several errors. 458 | First error at line 3. 459 | >>> len(e.errors) == 2 460 | 1 461 | >>> the_error = e.errors[1] 462 | >>> assert(isinstance(the_error, UnreprError)) 463 | 464 | >>> unknown_name = ''' 465 | ... key = "value" 466 | ... key2 = value 467 | ... '''.splitlines() 468 | >>> c = ConfigObj(unknown_name) 469 | >>> c = ConfigObj(unknown_name, unrepr=True) 470 | Traceback (most recent call last): 471 | UnreprError: Unknown name or type in value at line 3. 472 | >>> c = ConfigObj(unknown_name, raise_errors=True, unrepr=True) 473 | Traceback (most recent call last): 474 | UnreprError: Unknown name or type in value at line 3. 475 | """ 476 | 477 | 478 | def _test_validate_with_copy_and_many(): 479 | """ 480 | >>> spec = ''' 481 | ... [section] 482 | ... [[__many__]] 483 | ... value = string(default='nothing') 484 | ... ''' 485 | >>> config = ''' 486 | ... [section] 487 | ... [[something]] 488 | ... ''' 489 | >>> c = ConfigObj(StringIO(config), configspec=StringIO(spec)) 490 | >>> v = Validator() 491 | >>> r = c.validate(v, copy=True) 492 | >>> c['section']['something']['value'] == 'nothing' 493 | True 494 | """ 495 | 496 | def _test_configspec_with_hash(): 497 | """ 498 | >>> spec = ['stuff = string(default="#ff00dd")'] 499 | >>> c = ConfigObj(spec, _inspec=True) 500 | >>> c['stuff'] 501 | 'string(default="#ff00dd")' 502 | >>> c = ConfigObj(configspec=spec) 503 | >>> v = Validator() 504 | >>> c.validate(v) 505 | 1 506 | >>> c['stuff'] 507 | '#ff00dd' 508 | 509 | 510 | >>> spec = ['stuff = string(default="fish") # wooble'] 511 | >>> c = ConfigObj(spec, _inspec=True) 512 | >>> c['stuff'] 513 | 'string(default="fish") # wooble' 514 | """ 515 | 516 | def _test_many_check(): 517 | """ 518 | >>> spec = ['__many__ = integer()'] 519 | >>> config = ['a = 6', 'b = 7'] 520 | >>> c = ConfigObj(config, configspec=spec) 521 | >>> v = Validator() 522 | >>> c.validate(v) 523 | 1 524 | >>> isinstance(c['a'], int) 525 | True 526 | >>> isinstance(c['b'], int) 527 | True 528 | 529 | 530 | >>> spec = ['[name]', '__many__ = integer()'] 531 | >>> config = ['[name]', 'a = 6', 'b = 7'] 532 | >>> c = ConfigObj(config, configspec=spec) 533 | >>> v = Validator() 534 | >>> c.validate(v) 535 | 1 536 | >>> isinstance(c['name']['a'], int) 537 | True 538 | >>> isinstance(c['name']['b'], int) 539 | True 540 | 541 | 542 | >>> spec = ['[__many__]', '__many__ = integer()'] 543 | >>> config = ['[name]', 'hello = 7', '[thing]', 'fish = 0'] 544 | >>> c = ConfigObj(config, configspec=spec) 545 | >>> v = Validator() 546 | >>> c.validate(v) 547 | 1 548 | >>> isinstance(c['name']['hello'], int) 549 | True 550 | >>> isinstance(c['thing']['fish'], int) 551 | True 552 | 553 | 554 | >>> spec = ''' 555 | ... ___many___ = integer 556 | ... [__many__] 557 | ... ___many___ = boolean 558 | ... [[__many__]] 559 | ... __many__ = float 560 | ... '''.splitlines() 561 | >>> config = ''' 562 | ... fish = 8 563 | ... buggle = 4 564 | ... [hi] 565 | ... one = true 566 | ... two = false 567 | ... [[bye]] 568 | ... odd = 3 569 | ... whoops = 9.0 570 | ... [bye] 571 | ... one = true 572 | ... two = true 573 | ... [[lots]] 574 | ... odd = 3 575 | ... whoops = 9.0 576 | ... '''.splitlines() 577 | >>> c = ConfigObj(config, configspec=spec) 578 | >>> v = Validator() 579 | >>> c.validate(v) 580 | 1 581 | >>> isinstance(c['fish'], int) 582 | True 583 | >>> isinstance(c['buggle'], int) 584 | True 585 | >>> c['hi']['one'] 586 | 1 587 | >>> c['hi']['two'] 588 | 0 589 | >>> isinstance(c['hi']['bye']['odd'], float) 590 | True 591 | >>> isinstance(c['hi']['bye']['whoops'], float) 592 | True 593 | >>> c['bye']['one'] 594 | 1 595 | >>> c['bye']['two'] 596 | 1 597 | >>> isinstance(c['bye']['lots']['odd'], float) 598 | True 599 | >>> isinstance(c['bye']['lots']['whoops'], float) 600 | True 601 | 602 | 603 | >>> spec = ['___many___ = integer()'] 604 | >>> config = ['a = 6', 'b = 7'] 605 | >>> c = ConfigObj(config, configspec=spec) 606 | >>> v = Validator() 607 | >>> c.validate(v) 608 | 1 609 | >>> isinstance(c['a'], int) 610 | True 611 | >>> isinstance(c['b'], int) 612 | True 613 | 614 | 615 | >>> spec = ''' 616 | ... [__many__] 617 | ... [[__many__]] 618 | ... __many__ = float 619 | ... '''.splitlines() 620 | >>> config = ''' 621 | ... [hi] 622 | ... [[bye]] 623 | ... odd = 3 624 | ... whoops = 9.0 625 | ... [bye] 626 | ... [[lots]] 627 | ... odd = 3 628 | ... whoops = 9.0 629 | ... '''.splitlines() 630 | >>> c = ConfigObj(config, configspec=spec) 631 | >>> v = Validator() 632 | >>> c.validate(v) 633 | 1 634 | >>> isinstance(c['hi']['bye']['odd'], float) 635 | True 636 | >>> isinstance(c['hi']['bye']['whoops'], float) 637 | True 638 | >>> isinstance(c['bye']['lots']['odd'], float) 639 | True 640 | >>> isinstance(c['bye']['lots']['whoops'], float) 641 | True 642 | 643 | >>> s = ['[dog]', '[[cow]]', 'something = boolean', '[[__many__]]', 644 | ... 'fish = integer'] 645 | >>> c = ['[dog]', '[[cow]]', 'something = true', '[[ob]]', 646 | ... 'fish = 3', '[[bo]]', 'fish = 6'] 647 | >>> ini = ConfigObj(c, configspec=s) 648 | >>> v = Validator() 649 | >>> ini.validate(v) 650 | 1 651 | >>> ini['dog']['cow']['something'] 652 | 1 653 | >>> ini['dog']['ob']['fish'] 654 | 3 655 | >>> ini['dog']['bo']['fish'] 656 | 6 657 | 658 | 659 | >>> s = ['[cow]', 'something = boolean', '[__many__]', 660 | ... 'fish = integer'] 661 | >>> c = ['[cow]', 'something = true', '[ob]', 662 | ... 'fish = 3', '[bo]', 'fish = 6'] 663 | >>> ini = ConfigObj(c, configspec=s) 664 | >>> v = Validator() 665 | >>> ini.validate(v) 666 | 1 667 | >>> ini['cow']['something'] 668 | 1 669 | >>> ini['ob']['fish'] 670 | 3 671 | >>> ini['bo']['fish'] 672 | 6 673 | """ 674 | 675 | 676 | def _unexpected_validation_errors(): 677 | """ 678 | Although the input is nonsensical we should not crash but correctly 679 | report the failure to validate 680 | 681 | # section specified, got scalar 682 | >>> from configobj.validate import ValidateError 683 | >>> s = ['[cow]', 'something = boolean'] 684 | >>> c = ['cow = true'] 685 | >>> ini = ConfigObj(c, configspec=s) 686 | >>> v = Validator() 687 | >>> ini.validate(v) 688 | 0 689 | 690 | >>> ini = ConfigObj(c, configspec=s) 691 | >>> res = ini.validate(v, preserve_errors=True) 692 | >>> check = flatten_errors(ini, res) 693 | >>> for entry in check: 694 | ... isinstance(entry[2], ValidateError) 695 | ... print(str(entry[2])) 696 | True 697 | Section 'cow' was provided as a single value 698 | 699 | 700 | # scalar specified, got section 701 | >>> s = ['something = boolean'] 702 | >>> c = ['[something]', 'cow = true'] 703 | >>> ini = ConfigObj(c, configspec=s) 704 | >>> v = Validator() 705 | >>> ini.validate(v) 706 | 0 707 | 708 | >>> ini = ConfigObj(c, configspec=s) 709 | >>> res = ini.validate(v, preserve_errors=True) 710 | >>> check = flatten_errors(ini, res) 711 | >>> for entry in check: 712 | ... isinstance(entry[2], ValidateError) 713 | ... print(str(entry[2])) 714 | True 715 | Value 'something' was provided as a section 716 | 717 | # unexpected section 718 | >>> s = [] 719 | >>> c = ['[cow]', 'dog = true'] 720 | >>> ini = ConfigObj(c, configspec=s) 721 | >>> v = Validator() 722 | >>> ini.validate(v) 723 | 1 724 | 725 | 726 | >>> s = ['[cow]', 'dog = boolean'] 727 | >>> c = ['[cow]', 'dog = true'] 728 | >>> ini = ConfigObj(c, configspec=s) 729 | >>> v = Validator() 730 | >>> ini.validate(v, preserve_errors=True) 731 | 1 732 | """ 733 | 734 | def _test_pickle(): 735 | """ 736 | >>> import pickle 737 | >>> s = ['[cow]', 'dog = boolean'] 738 | >>> c = ['[cow]', 'dog = true'] 739 | >>> ini = ConfigObj(c, configspec=s) 740 | >>> v = Validator() 741 | >>> string = pickle.dumps(ini) 742 | >>> new = pickle.loads(string) 743 | >>> new.validate(v) 744 | 1 745 | """ 746 | 747 | def _test_as_list(): 748 | """ 749 | >>> a = ConfigObj() 750 | >>> a['a'] = 1 751 | >>> a.as_list('a') 752 | [1] 753 | >>> a['a'] = (1,) 754 | >>> a.as_list('a') 755 | [1] 756 | >>> a['a'] = [1] 757 | >>> a.as_list('a') 758 | [1] 759 | """ 760 | 761 | def _test_list_interpolation(): 762 | """ 763 | >>> c = ConfigObj() 764 | >>> c['x'] = 'foo' 765 | >>> c['list'] = ['%(x)s', 3] 766 | >>> c['list'] 767 | ['foo', 3] 768 | """ 769 | 770 | def _test_extra_values(): 771 | """ 772 | >>> spec = ['[section]'] 773 | >>> infile = ['bar = 3', '[something]', 'foo = fish', '[section]', 'foo=boo'] 774 | >>> c = ConfigObj(infile, configspec=spec) 775 | >>> c.extra_values 776 | [] 777 | >>> c.extra_values = ['bar', 'gosh', 'what'] 778 | >>> c.validate(Validator()) 779 | 1 780 | >>> c.extra_values 781 | ['bar', 'something'] 782 | >>> c['section'].extra_values 783 | ['foo'] 784 | >>> c['something'].extra_values 785 | [] 786 | """ 787 | 788 | def _test_reset_and_clear_more(): 789 | """ 790 | >>> c = ConfigObj() 791 | >>> c.extra_values = ['foo'] 792 | >>> c.defaults = ['bar'] 793 | >>> c.default_values = {'bar': 'baz'} 794 | >>> c.clear() 795 | >>> c.defaults 796 | [] 797 | >>> c.extra_values 798 | [] 799 | >>> c.default_values 800 | {'bar': 'baz'} 801 | >>> c.extra_values = ['foo'] 802 | >>> c.defaults = ['bar'] 803 | >>> c.reset() 804 | >>> c.defaults 805 | [] 806 | >>> c.extra_values 807 | [] 808 | >>> c.default_values 809 | {} 810 | """ 811 | 812 | def _test_invalid_lists(): 813 | """ 814 | >>> v = ['string = val, val2, , val3'] 815 | >>> c = ConfigObj(v) 816 | Traceback (most recent call last): 817 | ParseError: Parse error in value at line 1. 818 | >>> v = ['string = val, val2,, val3'] 819 | >>> c = ConfigObj(v) 820 | Traceback (most recent call last): 821 | ParseError: Parse error in value at line 1. 822 | >>> v = ['string = val, val2,,'] 823 | >>> c = ConfigObj(v) 824 | Traceback (most recent call last): 825 | ParseError: Parse error in value at line 1. 826 | >>> v = ['string = val, ,'] 827 | >>> c = ConfigObj(v) 828 | Traceback (most recent call last): 829 | ParseError: Parse error in value at line 1. 830 | >>> v = ['string = val, , '] 831 | >>> c = ConfigObj(v) 832 | Traceback (most recent call last): 833 | ParseError: Parse error in value at line 1. 834 | >>> v = ['string = ,,'] 835 | >>> c = ConfigObj(v) 836 | Traceback (most recent call last): 837 | ParseError: Parse error in value at line 1. 838 | >>> v = ['string = ,, '] 839 | >>> c = ConfigObj(v) 840 | Traceback (most recent call last): 841 | ParseError: Parse error in value at line 1. 842 | >>> v = ['string = ,foo'] 843 | >>> c = ConfigObj(v) 844 | Traceback (most recent call last): 845 | ParseError: Parse error in value at line 1. 846 | >>> v = ['string = foo, '] 847 | >>> c = ConfigObj(v) 848 | >>> c['string'] 849 | ['foo'] 850 | >>> v = ['string = foo, "'] 851 | >>> c = ConfigObj(v) 852 | Traceback (most recent call last): 853 | ParseError: Parse error in value at line 1. 854 | """ 855 | 856 | def _test_validation_with_preserve_errors(): 857 | """ 858 | >>> v = Validator() 859 | >>> spec = ['[section]', 'foo = integer'] 860 | >>> c = ConfigObj(configspec=spec) 861 | >>> c.validate(v, preserve_errors=True) 862 | {'section': False} 863 | >>> c = ConfigObj(['[section]'], configspec=spec) 864 | >>> c.validate(v) 865 | False 866 | >>> c.validate(v, preserve_errors=True) 867 | {'section': {'foo': False}} 868 | """ 869 | 870 | 871 | # test _created on Section 872 | 873 | # TODO: Test BOM handling 874 | # TODO: Test error code for badly built multiline values 875 | # TODO: Test handling of StringIO 876 | # TODO: Test interpolation with writing 877 | 878 | 879 | if __name__ == '__main__': 880 | # run the code tests in doctest format 881 | # 882 | testconfig1 = """\ 883 | key1= val # comment 1 884 | key2= val # comment 2 885 | # comment 3 886 | [lev1a] # comment 4 887 | key1= val # comment 5 888 | key2= val # comment 6 889 | # comment 7 890 | [lev1b] # comment 8 891 | key1= val # comment 9 892 | key2= val # comment 10 893 | # comment 11 894 | [[lev2ba]] # comment 12 895 | key1= val # comment 13 896 | # comment 14 897 | [[lev2bb]] # comment 15 898 | key1= val # comment 16 899 | # comment 17 900 | [lev1c] # comment 18 901 | # comment 19 902 | [[lev2c]] # comment 20 903 | # comment 21 904 | [[[lev3c]]] # comment 22 905 | key1 = val # comment 23""" 906 | # 907 | testconfig2 = b"""\ 908 | key1 = 'val1' 909 | key2 = "val2" 910 | key3 = val3 911 | ["section 1"] # comment 912 | keys11 = val1 913 | keys12 = val2 914 | keys13 = val3 915 | [section 2] 916 | keys21 = val1 917 | keys22 = val2 918 | keys23 = val3 919 | 920 | [['section 2 sub 1']] 921 | fish = 3 922 | """ 923 | # 924 | testconfig6 = b''' 925 | name1 = """ a single line value """ # comment 926 | name2 = \''' another single line value \''' # comment 927 | name3 = """ a single line value """ 928 | name4 = \''' another single line value \''' 929 | [ "multi section" ] 930 | name1 = """ 931 | Well, this is a 932 | multiline value 933 | """ 934 | name2 = \''' 935 | Well, this is a 936 | multiline value 937 | \''' 938 | name3 = """ 939 | Well, this is a 940 | multiline value 941 | """ # a comment 942 | name4 = \''' 943 | Well, this is a 944 | multiline value 945 | \''' # I guess this is a comment too 946 | ''' 947 | # 948 | # these cannot be put among the doctests, because the doctest module 949 | # does a string.expandtabs() on all of them, sigh 950 | # oneTabCfg = ['[sect]', '\t[[sect]]', '\t\tfoo = bar'] 951 | # twoTabsCfg = ['[sect]', '\t\t[[sect]]', '\t\t\t\tfoo = bar'] 952 | # tabsAndSpacesCfg = [b'[sect]', b'\t \t [[sect]]', b'\t \t \t \t foo = bar'] 953 | # 954 | import doctest 955 | m = sys.modules.get('__main__') 956 | globs = m.__dict__.copy() 957 | a = ConfigObj(testconfig1.split('\n'), raise_errors=True) 958 | b = ConfigObj(testconfig2.split(b'\n'), raise_errors=True) 959 | i = ConfigObj(testconfig6.split(b'\n'), raise_errors=True) 960 | globs.update({'a': a, 'b': b, 'i': i}) 961 | pre_failures, pre_tests = doctest.testmod( 962 | m, globs=globs, 963 | optionflags=doctest.IGNORE_EXCEPTION_DETAIL | doctest.ELLIPSIS) 964 | 965 | import configobj 966 | post_failures, post_tests = doctest.testmod( 967 | configobj, globs=globs, 968 | optionflags=doctest.IGNORE_EXCEPTION_DETAIL | doctest.ELLIPSIS) 969 | assert not (pre_failures or post_failures), ( 970 | '{} failures out of {} tests'.format(post_failures + pre_failures, 971 | post_tests + pre_tests)) 972 | 973 | 974 | # Man alive I prefer unittest ;-) 975 | -------------------------------------------------------------------------------- /src/tests/conftest.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import pytest 3 | 4 | from configobj import ConfigObj 5 | from configobj.validate import Validator 6 | 7 | @pytest.fixture 8 | def empty_cfg(): 9 | return ConfigObj() 10 | 11 | 12 | @pytest.fixture 13 | def val(): 14 | return Validator() 15 | -------------------------------------------------------------------------------- /src/tests/test_configobj.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import unicode_literals 3 | import os 4 | import re 5 | 6 | from codecs import BOM_UTF8 7 | from pathlib import Path 8 | from warnings import catch_warnings 9 | from tempfile import NamedTemporaryFile 10 | 11 | import pytest 12 | import io 13 | 14 | import configobj as co 15 | from configobj import ConfigObj, flatten_errors, ReloadError, DuplicateError, MissingInterpolationOption, InterpolationLoopError, ConfigObjError 16 | from configobj.validate import Validator, VdtValueTooSmallError 17 | 18 | 19 | def cfg_lines(config_string_representation): 20 | """ 21 | :param config_string_representation: string representation of a config 22 | file (typically a triple-quoted string) 23 | :type config_string_representation: str or unicode 24 | :return: a list of lines of that config. Whitespace on the left will be 25 | trimmed based on the indentation level to make it a bit saner to assert 26 | content of a particular line 27 | :rtype: str or unicode 28 | """ 29 | lines = config_string_representation.splitlines() 30 | 31 | for idx, line in enumerate(lines): 32 | if line.strip(): 33 | line_no_with_content = idx 34 | break 35 | else: 36 | raise ValueError('no content in provided config file: ' 37 | '{!r}'.format(config_string_representation)) 38 | 39 | first_content = lines[line_no_with_content] 40 | if isinstance(first_content, bytes): 41 | first_content = first_content.decode('utf-8') 42 | ws_chars = len(re.search(r'^(\s*)', first_content).group(1)) 43 | 44 | def yield_stringified_line(): 45 | for line in lines: 46 | if isinstance(line, bytes): 47 | yield line.decode('utf-8') 48 | else: 49 | yield line 50 | 51 | 52 | return [re.sub(r'^\s{0,%s}' % ws_chars, '', line).encode('utf-8') 53 | for line in yield_stringified_line()] 54 | 55 | 56 | @pytest.fixture 57 | def cfg_contents(request): 58 | 59 | def make_file_with_contents_and_return_name(config_string_representation): 60 | """ 61 | :param config_string_representation: string representation of a config 62 | file (typically a triple-quoted string) 63 | :type config_string_representation: str or unicode 64 | :return: a list of lines of that config. Whitespace on the left will be 65 | trimmed based on the indentation level to make it a bit saner to assert 66 | content of a particular line 67 | :rtype: basestring 68 | """ 69 | 70 | lines = cfg_lines(config_string_representation) 71 | 72 | with NamedTemporaryFile(delete=False, mode='wb') as cfg_file: 73 | for line in lines: 74 | if isinstance(line, bytes): 75 | cfg_file.write(line + os.linesep.encode('utf-8')) 76 | else: 77 | cfg_file.write((line + os.linesep).encode('utf-8')) 78 | request.addfinalizer(lambda : os.unlink(cfg_file.name)) 79 | 80 | return cfg_file.name 81 | 82 | return make_file_with_contents_and_return_name 83 | 84 | 85 | def test_order_preserved(): 86 | c = ConfigObj() 87 | c['a'] = 1 88 | c['b'] = 2 89 | c['c'] = 3 90 | c['section'] = {} 91 | c['section']['a'] = 1 92 | c['section']['b'] = 2 93 | c['section']['c'] = 3 94 | c['section']['section'] = {} 95 | c['section']['section2'] = {} 96 | c['section']['section3'] = {} 97 | c['section2'] = {} 98 | c['section3'] = {} 99 | 100 | c2 = ConfigObj(c) 101 | assert c2.scalars == ['a', 'b', 'c'] 102 | assert c2.sections == ['section', 'section2', 'section3'] 103 | assert c2['section'].scalars == ['a', 'b', 'c'] 104 | assert c2['section'].sections == ['section', 'section2', 'section3'] 105 | 106 | assert c['section'] is not c2['section'] 107 | assert c['section']['section'] is not c2['section']['section'] 108 | 109 | 110 | def test_options_deprecation(): 111 | with catch_warnings(record=True) as log: 112 | ConfigObj(options={}) 113 | 114 | # unpack the only member of log 115 | try: 116 | warning, = log 117 | except ValueError: 118 | assert len(log) == 1 119 | 120 | assert warning.category == DeprecationWarning 121 | 122 | 123 | def test_list_members(): 124 | c = ConfigObj() 125 | c['a'] = [] 126 | c['a'].append('foo') 127 | assert c['a'] == ['foo'] 128 | 129 | 130 | def test_list_interpolation_with_pop(): 131 | c = ConfigObj() 132 | c['a'] = [] 133 | c['a'].append('%(b)s') 134 | c['b'] = 'bar' 135 | assert c.pop('a') == ['bar'] 136 | 137 | 138 | def test_with_default(): 139 | c = ConfigObj() 140 | c['a'] = 3 141 | 142 | assert c.pop('a') == 3 143 | assert c.pop('b', 3) == 3 144 | with pytest.raises(KeyError): 145 | c.pop('c') 146 | 147 | 148 | def test_interpolation_with_section_names(cfg_contents): 149 | cfg = cfg_contents(""" 150 | item1 = 1234 151 | [section] 152 | [[item1]] 153 | foo='bar' 154 | [[DEFAULT]] 155 | [[[item1]]] 156 | why = would you do this? 157 | [[other-subsection]] 158 | item2 = '$item1'""") 159 | c = ConfigObj(cfg, interpolation='Template') 160 | 161 | # This raises an exception in 4.7.1 and earlier due to the section 162 | # being found as the interpolation value 163 | repr(c) 164 | 165 | 166 | def test_interoplation_repr(): 167 | c = ConfigObj(['foo = $bar'], interpolation='Template') 168 | c['baz'] = {} 169 | c['baz']['spam'] = '%(bar)s' 170 | 171 | # This raises a MissingInterpolationOption exception in 4.7.1 and earlier 172 | repr(c) 173 | 174 | 175 | class TestEncoding(object): 176 | @pytest.fixture 177 | def ant_cfg(self): 178 | return """ 179 | [tags] 180 | [[bug]] 181 | translated = \U0001f41c 182 | """ 183 | 184 | #issue #18 185 | def test_unicode_conversion_when_encoding_is_set(self, cfg_contents): 186 | cfg = cfg_contents(b"test = some string") 187 | 188 | c = ConfigObj(cfg, encoding='utf8') 189 | 190 | assert isinstance(c['test'], str) 191 | 192 | 193 | #issue #18 194 | def test_no_unicode_conversion_when_encoding_is_omitted(self, cfg_contents): 195 | cfg = cfg_contents(b"test = some string") 196 | 197 | c = ConfigObj(cfg) 198 | assert isinstance(c['test'], str) 199 | 200 | #issue #44 201 | def test_that_encoding_using_list_of_strings(self): 202 | cfg = [b'test = \xf0\x9f\x90\x9c'] 203 | 204 | c = ConfigObj(cfg, encoding='utf8') 205 | 206 | assert isinstance(c['test'], str) 207 | 208 | assert c['test'] == '\U0001f41c' 209 | 210 | #issue #44 211 | def test_encoding_in_subsections(self, ant_cfg, cfg_contents): 212 | c = cfg_contents(ant_cfg) 213 | cfg = ConfigObj(c, encoding='utf-8') 214 | 215 | assert isinstance(cfg['tags']['bug']['translated'], str) 216 | 217 | #issue #44 and #55 218 | def test_encoding_in_config_files(self, request, ant_cfg): 219 | # the cfg_contents fixture is doing this too, but be explicit 220 | with NamedTemporaryFile(delete=False, mode='wb') as cfg_file: 221 | cfg_file.write(ant_cfg.encode('utf-8')) 222 | request.addfinalizer(lambda : os.unlink(cfg_file.name)) 223 | 224 | cfg = ConfigObj(cfg_file.name, encoding='utf-8') 225 | assert isinstance(cfg['tags']['bug']['translated'], str) 226 | cfg.write() 227 | 228 | @pytest.fixture 229 | def testconfig1(): 230 | """ 231 | copied from the main doctest 232 | """ 233 | return """\ 234 | key1= val # comment 1 235 | key2= val # comment 2 236 | # comment 3 237 | [lev1a] # comment 4 238 | key1= val # comment 5 239 | key2= val # comment 6 240 | # comment 7 241 | [lev1b] # comment 8 242 | key1= val # comment 9 243 | key2= val # comment 10 244 | # comment 11 245 | [[lev2ba]] # comment 12 246 | key1= val # comment 13 247 | # comment 14 248 | [[lev2bb]] # comment 15 249 | key1= val # comment 16 250 | # comment 17 251 | [lev1c] # comment 18 252 | # comment 19 253 | [[lev2c]] # comment 20 254 | # comment 21 255 | [[[lev3c]]] # comment 22 256 | key1 = val # comment 23""" 257 | 258 | 259 | @pytest.fixture 260 | def testconfig2(): 261 | return """\ 262 | key1 = 'val1' 263 | key2 = "val2" 264 | key3 = val3 265 | ["section 1"] # comment 266 | keys11 = val1 267 | keys12 = val2 268 | keys13 = val3 269 | [section 2] 270 | keys21 = val1 271 | keys22 = val2 272 | keys23 = val3 273 | 274 | [['section 2 sub 1']] 275 | fish = 3 276 | """ 277 | 278 | 279 | @pytest.fixture 280 | def testconfig6(): 281 | return b''' 282 | name1 = """ a single line value """ # comment 283 | name2 = \''' another single line value \''' # comment 284 | name3 = """ a single line value """ 285 | name4 = \''' another single line value \''' 286 | [ "multi section" ] 287 | name1 = """ 288 | Well, this is a 289 | multiline value 290 | """ 291 | name2 = \''' 292 | Well, this is a 293 | multiline value 294 | \''' 295 | name3 = """ 296 | Well, this is a 297 | multiline value 298 | """ # a comment 299 | name4 = \''' 300 | Well, this is a 301 | multiline value 302 | \''' # I guess this is a comment too 303 | ''' 304 | 305 | 306 | @pytest.fixture 307 | def a(testconfig1, cfg_contents): 308 | """ 309 | also copied from main doc tests 310 | """ 311 | return ConfigObj(cfg_contents(testconfig1), raise_errors=True) 312 | 313 | 314 | @pytest.fixture 315 | def b(testconfig2, cfg_contents): 316 | """ 317 | also copied from main doc tests 318 | """ 319 | return ConfigObj(cfg_contents(testconfig2), raise_errors=True) 320 | 321 | 322 | @pytest.fixture 323 | def i(testconfig6, cfg_contents): 324 | """ 325 | also copied from main doc tests 326 | """ 327 | return ConfigObj(cfg_contents(testconfig6), raise_errors=True) 328 | 329 | 330 | def test_configobj_dict_representation(a, b, cfg_contents): 331 | 332 | assert a.depth == 0 333 | assert a == { 334 | 'key2': 'val', 335 | 'key1': 'val', 336 | 'lev1c': { 337 | 'lev2c': { 338 | 'lev3c': { 339 | 'key1': 'val', 340 | }, 341 | }, 342 | }, 343 | 'lev1b': { 344 | 'key2': 'val', 345 | 'key1': 'val', 346 | 'lev2ba': { 347 | 'key1': 'val', 348 | }, 349 | 'lev2bb': { 350 | 'key1': 'val', 351 | }, 352 | }, 353 | 'lev1a': { 354 | 'key2': 'val', 355 | 'key1': 'val', 356 | }, 357 | } 358 | 359 | assert b.depth == 0 360 | assert b == { 361 | 'key3': 'val3', 362 | 'key2': 'val2', 363 | 'key1': 'val1', 364 | 'section 1': { 365 | 'keys11': 'val1', 366 | 'keys13': 'val3', 367 | 'keys12': 'val2', 368 | }, 369 | 'section 2': { 370 | 'section 2 sub 1': { 371 | 'fish': '3', 372 | }, 373 | 'keys21': 'val1', 374 | 'keys22': 'val2', 375 | 'keys23': 'val3', 376 | }, 377 | } 378 | 379 | t = cfg_lines(""" 380 | 'a' = b # !"$%^&*(),::;'@~#= 33 381 | "b" = b #= 6, 33 382 | """) 383 | t2 = ConfigObj(t) 384 | assert t2 == {'a': 'b', 'b': 'b'} 385 | t2.inline_comments['b'] = '' 386 | del t2['a'] 387 | assert t2.write() == ['','b = b', ''] 388 | 389 | 390 | def test_behavior_when_list_values_is_false(): 391 | c = ''' 392 | key1 = no quotes 393 | key2 = 'single quotes' 394 | key3 = "double quotes" 395 | key4 = "list", 'with', several, "quotes" 396 | ''' 397 | cfg = ConfigObj(cfg_lines(c), list_values=False) 398 | assert cfg == { 399 | 'key1': 'no quotes', 400 | 'key2': "'single quotes'", 401 | 'key3': '"double quotes"', 402 | 'key4': '"list", \'with\', several, "quotes"' 403 | } 404 | 405 | cfg2 = ConfigObj(list_values=False) 406 | cfg2['key1'] = 'Multiline\nValue' 407 | cfg2['key2'] = '''"Value" with 'quotes' !''' 408 | assert cfg2.write() == [ 409 | "key1 = '''Multiline\nValue'''", 410 | 'key2 = "Value" with \'quotes\' !' 411 | ] 412 | 413 | cfg2.list_values = True 414 | assert cfg2.write() == [ 415 | "key1 = '''Multiline\nValue'''", 416 | 'key2 = \'\'\'"Value" with \'quotes\' !\'\'\'' 417 | ] 418 | 419 | 420 | def test_flatten_errors(val, cfg_contents): 421 | config = cfg_contents(""" 422 | test1=40 423 | test2=hello 424 | test3=3 425 | test4=5.0 426 | [section] 427 | test1=40 428 | test2=hello 429 | test3=3 430 | test4=5.0 431 | [[sub section]] 432 | test1=40 433 | test2=hello 434 | test3=3 435 | test4=5.0 436 | """) 437 | configspec = cfg_contents(""" 438 | test1= integer(30,50) 439 | test2= string 440 | test3=integer 441 | test4=float(6.0) 442 | [section] 443 | test1=integer(30,50) 444 | test2=string 445 | test3=integer 446 | test4=float(6.0) 447 | [[sub section]] 448 | test1=integer(30,50) 449 | test2=string 450 | test3=integer 451 | test4=float(6.0) 452 | """) 453 | c1 = ConfigObj(config, configspec=configspec) 454 | res = c1.validate(val) 455 | assert flatten_errors(c1, res) == [([], 'test4', False), (['section'], 'test4', False), (['section', 'sub section'], 'test4', False)] 456 | res = c1.validate(val, preserve_errors=True) 457 | check = flatten_errors(c1, res) 458 | assert check[0][:2] == ([], 'test4') 459 | assert check[1][:2] == (['section'], 'test4') 460 | assert check[2][:2] == (['section', 'sub section'], 'test4') 461 | for entry in check: 462 | assert isinstance(entry[2], VdtValueTooSmallError) 463 | assert str(entry[2]) == 'the value "5.0" is too small.' 464 | 465 | 466 | def test_unicode_handling(): 467 | u_base = ''' 468 | # initial comment 469 | # inital comment 2 470 | test1 = some value 471 | # comment 472 | test2 = another value # inline comment 473 | # section comment 474 | [section] # inline comment 475 | test = test # another inline comment 476 | test2 = test2 477 | # final comment 478 | # final comment2 479 | ''' 480 | 481 | # needing to keep line endings means this isn't a good candidate 482 | # for the cfg_lines utility method 483 | u = u_base.encode('utf_8').splitlines(True) 484 | u[0] = BOM_UTF8 + u[0] 485 | uc = ConfigObj(u) 486 | uc.encoding = None 487 | assert uc.BOM 488 | assert uc == {'test1': 'some value', 'test2': 'another value', 489 | 'section': {'test': 'test', 'test2': 'test2'}} 490 | uc = ConfigObj(u, encoding='utf_8', default_encoding='latin-1') 491 | assert uc.BOM 492 | assert isinstance(uc['test1'], str) 493 | assert uc.encoding == 'utf_8' 494 | assert uc.newlines == '\n' 495 | assert len(uc.write()) == 13 496 | uc['latin1'] = "This costs lot's of " 497 | a_list = uc.write() 498 | assert 'latin1' in str(a_list) 499 | assert len(a_list) == 14 500 | assert isinstance(a_list[0], bytes) 501 | assert a_list[0].startswith(BOM_UTF8) 502 | 503 | u = u_base.replace('\n', '\r\n').encode('utf-8').splitlines(True) 504 | uc = ConfigObj(u) 505 | assert uc.newlines == '\r\n' 506 | uc.newlines = '\r' 507 | file_like = io.BytesIO() 508 | uc.write(file_like) 509 | file_like.seek(0) 510 | uc2 = ConfigObj(file_like) 511 | assert uc2 == uc 512 | assert uc2.filename == None 513 | assert uc2.newlines == '\r' 514 | 515 | 516 | class TestWritingConfigs(object): 517 | def test_validate(self, val): 518 | spec = [ 519 | '# Initial Comment', 520 | '', 521 | 'key1 = string(default=Hello)', 522 | '', 523 | '# section comment', 524 | '[section] # inline comment', 525 | '# key1 comment', 526 | 'key1 = integer(default=6)', 527 | '# key2 comment', 528 | 'key2 = boolean(default=True)', 529 | '# subsection comment', 530 | '[[sub-section]] # inline comment', 531 | '# another key1 comment', 532 | 'key1 = float(default=3.0)' 533 | ] 534 | blank_config = ConfigObj(configspec=spec) 535 | assert blank_config.validate(val, copy=True) 536 | assert blank_config.dict() == { 537 | 'key1': 'Hello', 538 | 'section': {'key1': 6, 'key2': True, 'sub-section': {'key1': 3.0}} 539 | } 540 | assert blank_config.write() == [ 541 | '# Initial Comment', 542 | '', 543 | 'key1 = Hello', 544 | '', 545 | '# section comment', 546 | '[section] # inline comment', 547 | '# key1 comment', 548 | 'key1 = 6', 549 | '# key2 comment', 550 | 'key2 = True', 551 | '# subsection comment', 552 | '[[sub-section]] # inline comment', 553 | '# another key1 comment', 554 | 'key1 = 3.0' 555 | ] 556 | 557 | def test_writing_empty_values(self): 558 | config_with_empty_values = [ 559 | '', 560 | 'key1 =', 561 | 'key2 =# a comment', 562 | ] 563 | cfg = ConfigObj(config_with_empty_values) 564 | assert cfg.write() == ['', 'key1 = ""', 'key2 = "" # a comment'] 565 | cfg.write_empty_values = True 566 | assert cfg.write() == ['', 'key1 = ', 'key2 = # a comment'] 567 | 568 | 569 | class TestUnrepr(object): 570 | def test_in_reading(self): 571 | config_to_be_unreprd = cfg_lines(""" 572 | key1 = (1, 2, 3) # comment 573 | key2 = True 574 | key3 = 'a string' 575 | key4 = [1, 2, 3, 'a mixed list'] 576 | """) 577 | cfg = ConfigObj(config_to_be_unreprd, unrepr=True) 578 | assert cfg == { 579 | 'key1': (1, 2, 3), 580 | 'key2': True, 581 | 'key3': 'a string', 582 | 'key4': [1, 2, 3, 'a mixed list'] 583 | } 584 | 585 | assert cfg == ConfigObj(cfg.write(), unrepr=True) 586 | 587 | def test_in_multiline_values(self, cfg_contents): 588 | config_with_multiline_value = cfg_contents(''' 589 | k = \"""{ 590 | 'k1': 3, 591 | 'k2': 6.0}\""" 592 | ''') 593 | cfg = ConfigObj(config_with_multiline_value, unrepr=True) 594 | assert cfg == {'k': {'k1': 3, 'k2': 6.0}} 595 | 596 | def test_with_a_dictionary(self): 597 | config_with_dict_value = ['k = {"a": 1}'] 598 | cfg = ConfigObj(config_with_dict_value, unrepr=True) 599 | assert isinstance(cfg['k'], dict) 600 | 601 | def test_with_hash(self): 602 | config_with_a_hash_in_a_list = [ 603 | 'key1 = (1, 2, 3) # comment', 604 | 'key2 = True', 605 | "key3 = 'a string'", 606 | "key4 = [1, 2, 3, 'a mixed list#']" 607 | ] 608 | cfg = ConfigObj(config_with_a_hash_in_a_list, unrepr=True) 609 | assert cfg == { 610 | 'key1': (1, 2, 3), 611 | 'key2': True, 612 | 'key3': 'a string', 613 | 'key4': [1, 2, 3, 'a mixed list#'] 614 | } 615 | 616 | 617 | class TestValueErrors(object): 618 | def test_bool(self, empty_cfg): 619 | empty_cfg['a'] = 'fish' 620 | with pytest.raises(ValueError) as excinfo: 621 | empty_cfg.as_bool('a') 622 | assert str(excinfo.value) == 'Value "fish" is neither True nor False' 623 | empty_cfg['b'] = 'True' 624 | assert empty_cfg.as_bool('b') is True 625 | empty_cfg['b'] = 'off' 626 | assert empty_cfg.as_bool('b') is False 627 | 628 | def test_int(self, empty_cfg): 629 | for bad in ('fish', '3.2'): 630 | empty_cfg['a'] = bad 631 | with pytest.raises(ValueError) as excinfo: 632 | empty_cfg.as_int('a') 633 | assert str(excinfo.value).startswith('invalid literal for int()') 634 | 635 | empty_cfg['b'] = '1' 636 | assert empty_cfg.as_bool('b') is True 637 | empty_cfg['b'] = '3.2' 638 | 639 | def test_float(self, empty_cfg): 640 | empty_cfg['a'] = 'fish' 641 | with pytest.raises(ValueError): 642 | empty_cfg.as_float('a') 643 | 644 | empty_cfg['b'] = '1' 645 | assert empty_cfg.as_float('b') == 1 646 | empty_cfg['b'] = '3.2' 647 | assert empty_cfg.as_float('b') == 3.2 648 | 649 | 650 | 651 | def test_error_types(): 652 | # errors that don't have interesting messages 653 | test_value = 'what' 654 | for ErrorClass in (co.ConfigObjError, co.NestingError, co.ParseError, 655 | co.DuplicateError, co.ConfigspecError, 656 | co.RepeatSectionError): 657 | with pytest.raises(ErrorClass) as excinfo: 658 | # TODO: assert more interesting things 659 | # now that we're not using doctest 660 | raise ErrorClass(test_value) 661 | assert str(excinfo.value) == test_value 662 | 663 | for ErrorClassWithMessage, msg in ( 664 | (co.InterpolationLoopError, 665 | 'interpolation loop detected in value "{0}".'), 666 | (co.MissingInterpolationOption, 667 | 'missing option "{0}" in interpolation.'), 668 | ): 669 | with pytest.raises(ErrorClassWithMessage) as excinfo: 670 | raise ErrorClassWithMessage(test_value) 671 | assert str(excinfo.value) == msg.format(test_value) 672 | 673 | # ReloadError is raised as IOError 674 | with pytest.raises(IOError): 675 | raise co.ReloadError() 676 | 677 | 678 | class TestSectionBehavior(object): 679 | def test_dictionary_representation(self, a): 680 | 681 | n = a.dict() 682 | assert n == a 683 | assert n is not a 684 | 685 | def test_merging(self, cfg_contents): 686 | config_with_subsection = cfg_contents(""" 687 | [section1] 688 | option1 = True 689 | [[subsection]] 690 | more_options = False 691 | # end of file 692 | """) 693 | config_that_overwrites_parameter = cfg_contents(""" 694 | # File is user.ini 695 | [section1] 696 | option1 = False 697 | # end of file 698 | """) 699 | c1 = ConfigObj(config_that_overwrites_parameter) 700 | c2 = ConfigObj(config_with_subsection) 701 | c2.merge(c1) 702 | assert c2.dict() == {'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}} 703 | 704 | def test_walking_with_in_place_updates(self, cfg_contents): 705 | config = cfg_contents(""" 706 | [XXXXsection] 707 | XXXXkey = XXXXvalue 708 | """) 709 | cfg = ConfigObj(config) 710 | assert cfg.dict() == {'XXXXsection': {'XXXXkey': 'XXXXvalue'}} 711 | def transform(section, key): 712 | val = section[key] 713 | newkey = key.replace('XXXX', 'CLIENT1') 714 | section.rename(key, newkey) 715 | if isinstance(val, str): 716 | val = val.replace('XXXX', 'CLIENT1') 717 | section[newkey] = val 718 | 719 | assert cfg.walk(transform, call_on_sections=True) == { 720 | 'CLIENT1section': {'CLIENT1key': None} 721 | } 722 | assert cfg.dict() == { 723 | 'CLIENT1section': {'CLIENT1key': 'CLIENT1value'} 724 | } 725 | 726 | 727 | def test_reset_a_configobj(): 728 | 729 | something = object() 730 | cfg = ConfigObj() 731 | cfg['something'] = something 732 | cfg['section'] = {'something': something} 733 | cfg.filename = 'fish' 734 | cfg.raise_errors = something 735 | cfg.list_values = something 736 | cfg.create_empty = something 737 | cfg.file_error = something 738 | cfg.stringify = something 739 | cfg.indent_type = something 740 | cfg.encoding = something 741 | cfg.default_encoding = something 742 | cfg.BOM = something 743 | cfg.newlines = something 744 | cfg.write_empty_values = something 745 | cfg.unrepr = something 746 | cfg.initial_comment = something 747 | cfg.final_comment = something 748 | cfg.configspec = something 749 | cfg.inline_comments = something 750 | cfg.comments = something 751 | cfg.defaults = something 752 | cfg.default_values = something 753 | cfg.reset() 754 | 755 | assert cfg.filename is None 756 | assert cfg.raise_errors is False 757 | assert cfg.list_values is True 758 | assert cfg.create_empty is False 759 | assert cfg.file_error is False 760 | assert cfg.interpolation is True 761 | assert cfg.configspec is None 762 | assert cfg.stringify is True 763 | assert cfg.indent_type is None 764 | assert cfg.encoding is None 765 | assert cfg.default_encoding is None 766 | assert cfg.unrepr is False 767 | assert cfg.write_empty_values is False 768 | assert cfg.inline_comments == {} 769 | assert cfg.comments == {} 770 | assert cfg.defaults == [] 771 | assert cfg.default_values == {} 772 | assert cfg == ConfigObj() 773 | assert repr(cfg) == 'ConfigObj({})' 774 | 775 | 776 | class TestReloading(object): 777 | @pytest.fixture 778 | def reloadable_cfg_content(self): 779 | content = ''' 780 | test1=40 781 | test2=hello 782 | test3=3 783 | test4=5.0 784 | [section] 785 | test1=40 786 | test2=hello 787 | test3=3 788 | test4=5.0 789 | [[sub section]] 790 | test1=40 791 | test2=hello 792 | test3=3 793 | test4=5.0 794 | [section2] 795 | test1=40 796 | test2=hello 797 | test3=3 798 | test4=5.0 799 | ''' 800 | return content 801 | 802 | def test_handle_no_filename(self): 803 | for bad_args in ([io.BytesIO()], [], [[]]): 804 | cfg = ConfigObj(*bad_args) 805 | with pytest.raises(ReloadError) as excinfo: 806 | cfg.reload() 807 | assert str(excinfo.value) == 'reload failed, filename is not set.' 808 | 809 | def test_reloading_with_an_actual_file(self, request, 810 | reloadable_cfg_content, 811 | cfg_contents): 812 | 813 | with NamedTemporaryFile(delete=False, mode='wb') as cfg_file: 814 | cfg_file.write(reloadable_cfg_content.encode('utf-8')) 815 | request.addfinalizer(lambda : os.unlink(cfg_file.name)) 816 | 817 | configspec = cfg_contents(""" 818 | test1= integer(30,50) 819 | test2= string 820 | test3=integer 821 | test4=float(4.5) 822 | [section] 823 | test1=integer(30,50) 824 | test2=string 825 | test3=integer 826 | test4=float(4.5) 827 | [[sub section]] 828 | test1=integer(30,50) 829 | test2=string 830 | test3=integer 831 | test4=float(4.5) 832 | [section2] 833 | test1=integer(30,50) 834 | test2=string 835 | test3=integer 836 | test4=float(4.5) 837 | """) 838 | 839 | cfg = ConfigObj(cfg_file.name, configspec=configspec) 840 | cfg.configspec['test1'] = 'integer(50,60)' 841 | backup = ConfigObj(cfg_file.name) 842 | del cfg['section'] 843 | del cfg['test1'] 844 | cfg['extra'] = '3' 845 | cfg['section2']['extra'] = '3' 846 | cfg.reload() 847 | assert cfg == backup 848 | assert cfg.validate(Validator()) 849 | 850 | 851 | class TestDuplicates(object): 852 | def test_duplicate_section(self): 853 | cfg = ''' 854 | [hello] 855 | member = value 856 | [hello again] 857 | member = value 858 | [ "hello" ] 859 | member = value 860 | ''' 861 | with pytest.raises(DuplicateError) as excinfo: 862 | ConfigObj(cfg.splitlines(), raise_errors=True) 863 | assert str(excinfo.value) == 'Duplicate section name at line 6.' 864 | 865 | def test_duplicate_members(self): 866 | d = ''' 867 | [hello] 868 | member=value 869 | [helloagain] 870 | member1=value 871 | member2=value 872 | 'member1'=value 873 | ["andagain"] 874 | member=value 875 | ''' 876 | with pytest.raises(DuplicateError) as excinfo: 877 | ConfigObj(d.splitlines(),raise_errors=True) 878 | assert str(excinfo.value) == 'Duplicate keyword name at line 7.' 879 | 880 | 881 | class TestInterpolation(object): 882 | """ 883 | tests various interpolation behaviors using config par 884 | """ 885 | @pytest.fixture 886 | def config_parser_cfg(self): 887 | cfg = ConfigObj() 888 | cfg['DEFAULT'] = { 889 | 'b': 'goodbye', 890 | 'userdir': r'c:\\home', 891 | 'c': '%(d)s', 892 | 'd': '%(c)s' 893 | } 894 | cfg['section'] = { 895 | 'a': r'%(datadir)s\\some path\\file.py', 896 | 'b': r'%(userdir)s\\some path\\file.py', 897 | 'c': 'Yo %(a)s', 898 | 'd': '%(not_here)s', 899 | 'e': '%(e)s', 900 | } 901 | cfg['section']['DEFAULT'] = { 902 | 'datadir': r'c:\\silly_test', 903 | 'a': 'hello - %(b)s', 904 | } 905 | return cfg 906 | 907 | @pytest.fixture 908 | def template_cfg(self, cfg_contents): 909 | interp_cfg = ''' 910 | [DEFAULT] 911 | keyword1 = value1 912 | 'keyword 2' = 'value 2' 913 | reference = ${keyword1} 914 | foo = 123 915 | 916 | [ section ] 917 | templatebare = $keyword1/foo 918 | bar = $$foo 919 | dollar = $$300.00 920 | stophere = $$notinterpolated 921 | with_braces = ${keyword1}s (plural) 922 | with_spaces = ${keyword 2}!!! 923 | with_several = $keyword1/$reference/$keyword1 924 | configparsersample = %(keyword 2)sconfig 925 | deep = ${reference} 926 | 927 | [[DEFAULT]] 928 | baz = $foo 929 | 930 | [[ sub-section ]] 931 | quux = '$baz + $bar + $foo' 932 | 933 | [[[ sub-sub-section ]]] 934 | convoluted = "$bar + $baz + $quux + $bar" 935 | ''' 936 | return ConfigObj(cfg_contents(interp_cfg), interpolation='Template') 937 | 938 | def test_interpolation(self, config_parser_cfg): 939 | test_section = config_parser_cfg['section'] 940 | assert test_section['a'] == r'c:\\silly_test\\some path\\file.py' 941 | assert test_section['b'] == r'c:\\home\\some path\\file.py' 942 | assert test_section['c'] == r'Yo c:\\silly_test\\some path\\file.py' 943 | 944 | def test_interpolation_turned_off(self, config_parser_cfg): 945 | config_parser_cfg.interpolation = False 946 | test_section = config_parser_cfg['section'] 947 | assert test_section['a'] == r'%(datadir)s\\some path\\file.py' 948 | assert test_section['b'] == r'%(userdir)s\\some path\\file.py' 949 | assert test_section['c'] == r'Yo %(a)s' 950 | 951 | def test_handle_errors(self, config_parser_cfg): 952 | 953 | with pytest.raises(MissingInterpolationOption) as excinfo: 954 | print(config_parser_cfg['section']['d']) 955 | assert (str(excinfo.value) == 956 | 'missing option "not_here" in interpolation.') 957 | 958 | with pytest.raises(InterpolationLoopError) as excinfo: 959 | print(config_parser_cfg['section']['e']) 960 | assert (str(excinfo.value) == 961 | 'interpolation loop detected in value "e".') 962 | 963 | def test_template_interpolation(self, template_cfg): 964 | test_sec = template_cfg['section'] 965 | assert test_sec['templatebare'] == 'value1/foo' 966 | assert test_sec['dollar'] == '$300.00' 967 | assert test_sec['stophere'] == '$notinterpolated' 968 | assert test_sec['with_braces'] == 'value1s (plural)' 969 | assert test_sec['with_spaces'] == 'value 2!!!' 970 | assert test_sec['with_several'] == 'value1/value1/value1' 971 | assert test_sec['configparsersample'] == '%(keyword 2)sconfig' 972 | assert test_sec['deep'] == 'value1' 973 | assert test_sec['sub-section']['quux'] == '123 + $foo + 123' 974 | assert (test_sec['sub-section']['sub-sub-section']['convoluted'] == 975 | '$foo + 123 + 123 + $foo + 123 + $foo') 976 | 977 | 978 | class TestQuotes(object): 979 | """ 980 | tests what happens whn dealing with quotes 981 | """ 982 | def assert_bad_quote_message(self, empty_cfg, to_quote, **kwargs): 983 | #TODO: this should be use repr instead of str 984 | message = 'Value "{0}" cannot be safely quoted.' 985 | with pytest.raises(ConfigObjError) as excinfo: 986 | empty_cfg._quote(to_quote, **kwargs) 987 | assert str(excinfo.value) == message.format(to_quote) 988 | 989 | def test_handle_unbalanced(self, i): 990 | self.assert_bad_quote_message(i, '"""\'\'\'') 991 | 992 | def test_handle_unallowed_newline(self, i): 993 | newline = '\n' 994 | self.assert_bad_quote_message(i, newline, multiline=False) 995 | 996 | def test_handle_unallowed_open_quote(self, i): 997 | open_quote = ' "\' ' 998 | self.assert_bad_quote_message(i, open_quote, multiline=False) 999 | 1000 | def test_handle_multiple_bad_quote_values(self): 1001 | testconfig5 = ''' 1002 | config = "hello # comment 1003 | test = 'goodbye 1004 | fish = 'goodbye # comment 1005 | dummy = "hello again 1006 | ''' 1007 | with pytest.raises(ConfigObjError) as excinfo: 1008 | ConfigObj(testconfig5.splitlines()) 1009 | assert len(excinfo.value.errors) == 4 1010 | 1011 | 1012 | 1013 | def test_handle_stringify_off(): 1014 | c = ConfigObj() 1015 | c.stringify = False 1016 | 1017 | with pytest.raises(TypeError) as excinfo: 1018 | c['test'] = 1 1019 | assert str(excinfo.value) == 'Value is not a string "1".' 1020 | 1021 | 1022 | class TestValues(object): 1023 | """ 1024 | Tests specifics about behaviors with types of values 1025 | """ 1026 | @pytest.fixture 1027 | def testconfig3(self, cfg_contents): 1028 | return cfg_contents(""" 1029 | a = , 1030 | b = test, 1031 | c = test1, test2 , test3 1032 | d = test1, test2, test3, 1033 | """) 1034 | 1035 | def test_empty_values(self, cfg_contents): 1036 | cfg_with_empty = cfg_contents(""" 1037 | k = 1038 | k2 =# comment test 1039 | val = test 1040 | val2 = , 1041 | val3 = 1, 1042 | val4 = 1, 2 1043 | val5 = 1, 2, """) 1044 | cwe = ConfigObj(cfg_with_empty) 1045 | # see a comma? it's a list 1046 | assert cwe == {'k': '', 'k2': '', 'val': 'test', 'val2': [], 1047 | 'val3': ['1'], 'val4': ['1', '2'], 'val5': ['1', '2']} 1048 | # not any more 1049 | cwe = ConfigObj(cfg_with_empty, list_values=False) 1050 | assert cwe == {'k': '', 'k2': '', 'val': 'test', 'val2': ',', 1051 | 'val3': '1,', 'val4': '1, 2', 'val5': '1, 2,'} 1052 | 1053 | def test_list_values(self, testconfig3): 1054 | cfg = ConfigObj(testconfig3, raise_errors=True) 1055 | assert cfg['a'] == [] 1056 | assert cfg['b'] == ['test'] 1057 | assert cfg['c'] == ['test1', 'test2', 'test3'] 1058 | assert cfg['d'] == ['test1', 'test2', 'test3'] 1059 | 1060 | def test_list_values_off(self, testconfig3): 1061 | cfg = ConfigObj(testconfig3, raise_errors=True, list_values=False) 1062 | assert cfg['a'] == ',' 1063 | assert cfg['b'] == 'test,' 1064 | assert cfg['c'] == 'test1, test2 , test3' 1065 | assert cfg['d'] == 'test1, test2, test3,' 1066 | 1067 | def test_handle_multiple_list_value_errors(self): 1068 | testconfig4 = ''' 1069 | config = 3,4,, 1070 | test = 3,,4 1071 | fish = ,, 1072 | dummy = ,,hello, goodbye 1073 | ''' 1074 | with pytest.raises(ConfigObjError) as excinfo: 1075 | ConfigObj(testconfig4.splitlines()) 1076 | assert len(excinfo.value.errors) == 4 1077 | 1078 | 1079 | 1080 | def test_creating_with_a_dictionary(): 1081 | dictionary_cfg_content = { 1082 | 'key1': 'val1', 1083 | 'key2': 'val2', 1084 | 'section 1': { 1085 | 'key1': 'val1', 1086 | 'key2': 'val2', 1087 | 'section 1b': { 1088 | 'key1': 'val1', 1089 | 'key2': 'val2', 1090 | }, 1091 | }, 1092 | 'section 2': { 1093 | 'key1': 'val1', 1094 | 'key2': 'val2', 1095 | 'section 2b': { 1096 | 'key1': 'val1', 1097 | 'key2': 'val2', 1098 | }, 1099 | }, 1100 | 'key3': 'val3', 1101 | } 1102 | cfg = ConfigObj(dictionary_cfg_content) 1103 | assert dictionary_cfg_content == cfg 1104 | assert dictionary_cfg_content is not cfg 1105 | assert dictionary_cfg_content == cfg.dict() 1106 | assert dictionary_cfg_content is not cfg.dict() 1107 | 1108 | def test_reading_a_pathlib_path(cfg_contents): 1109 | cfg = cfg_contents(""" 1110 | [section] 1111 | foo = bar""") 1112 | c = ConfigObj(Path(cfg)) 1113 | assert 'foo' in c['section'] 1114 | 1115 | def test_creating_a_file_from_pathlib_path(tmp_path): 1116 | infile = tmp_path / 'config.ini' 1117 | assert not Path(tmp_path / 'config.ini').is_file() 1118 | c = ConfigObj(Path(infile), create_empty=True) 1119 | assert Path(tmp_path / 'config.ini').is_file() 1120 | 1121 | def test_creating_a_file_from_string(tmp_path): 1122 | infile = str(tmp_path / 'config.ini') 1123 | assert not Path(infile).is_file() 1124 | c = ConfigObj(infile, create_empty=True) 1125 | assert Path(infile).is_file() 1126 | 1127 | class TestComments(object): 1128 | @pytest.fixture 1129 | def comment_filled_cfg(self, cfg_contents): 1130 | return cfg_contents(""" 1131 | # initial comments 1132 | # with two lines 1133 | key = "value" 1134 | # section comment 1135 | [section] # inline section comment 1136 | # key comment 1137 | key = "value" 1138 | 1139 | # final comment 1140 | # with two lines""" 1141 | ) 1142 | 1143 | def test_multiline_comments(self, i): 1144 | 1145 | expected_multiline_value = '\nWell, this is a\nmultiline value\n' 1146 | assert i == { 1147 | 'name4': ' another single line value ', 1148 | 'multi section': { 1149 | 'name4': expected_multiline_value, 1150 | 'name2': expected_multiline_value, 1151 | 'name3': expected_multiline_value, 1152 | 'name1': expected_multiline_value, 1153 | }, 1154 | 'name2': ' another single line value ', 1155 | 'name3': ' a single line value ', 1156 | 'name1': ' a single line value ', 1157 | } 1158 | 1159 | def test_starting_and_ending_comments(self, a, testconfig1, cfg_contents): 1160 | 1161 | filename = a.filename 1162 | a.filename = None 1163 | values = a.write() 1164 | index = 0 1165 | while index < 23: 1166 | index += 1 1167 | line = values[index-1] 1168 | assert line.endswith('# comment ' + str(index)) 1169 | a.filename = filename 1170 | 1171 | start_comment = ['# Initial Comment', '', '#'] 1172 | end_comment = ['', '#', '# Final Comment'] 1173 | newconfig = start_comment + testconfig1.splitlines() + end_comment 1174 | nc = ConfigObj(newconfig) 1175 | assert nc.initial_comment == ['# Initial Comment', '', '#'] 1176 | assert nc.final_comment == ['', '#', '# Final Comment'] 1177 | assert nc.initial_comment == start_comment 1178 | assert nc.final_comment == end_comment 1179 | 1180 | def test_inline_comments(self): 1181 | c = ConfigObj() 1182 | c['foo'] = 'bar' 1183 | c.inline_comments['foo'] = 'Nice bar' 1184 | assert c.write() == ['foo = bar # Nice bar'] 1185 | 1186 | def test_inline_comments_with_leading_number_sign(self): 1187 | c = ConfigObj() 1188 | c['foo'] = 'bar' 1189 | c.inline_comments['foo'] = '# Nice bar' 1190 | assert c.write() == ['foo = bar # Nice bar'] 1191 | 1192 | def test_unrepr_comments(self, comment_filled_cfg): 1193 | c = ConfigObj(comment_filled_cfg, unrepr=True) 1194 | assert c == { 'key': 'value', 'section': { 'key': 'value'}} 1195 | assert c.initial_comment == [ 1196 | '', '# initial comments', '# with two lines' 1197 | ] 1198 | assert c.comments == {'section': ['# section comment'], 'key': []} 1199 | assert c.inline_comments == { 1200 | 'section': '# inline section comment', 'key': '' 1201 | } 1202 | assert c['section'].comments == { 'key': ['# key comment']} 1203 | assert c.final_comment == ['', '# final comment', '# with two lines'] 1204 | 1205 | def test_comments(self, comment_filled_cfg): 1206 | c = ConfigObj(comment_filled_cfg) 1207 | assert c == { 'key': 'value', 'section': { 'key': 'value'}} 1208 | assert c.initial_comment == [ 1209 | '', '# initial comments', '# with two lines' 1210 | ] 1211 | assert c.comments == {'section': ['# section comment'], 'key': []} 1212 | assert c.inline_comments == { 1213 | 'section': '# inline section comment', 'key': None 1214 | } 1215 | assert c['section'].comments == { 'key': ['# key comment']} 1216 | assert c.final_comment == ['', '# final comment', '# with two lines'] 1217 | 1218 | 1219 | 1220 | def test_overwriting_filenames(a, b, i): 1221 | #TODO: I'm not entirely sure what this test is actually asserting 1222 | filename = a.filename 1223 | a.filename = 'test.ini' 1224 | a.write() 1225 | a.filename = filename 1226 | assert a == ConfigObj('test.ini', raise_errors=True) 1227 | os.remove('test.ini') 1228 | b.filename = 'test.ini' 1229 | b.write() 1230 | assert b == ConfigObj('test.ini', raise_errors=True) 1231 | os.remove('test.ini') 1232 | i.filename = 'test.ini' 1233 | i.write() 1234 | assert i == ConfigObj('test.ini', raise_errors=True) 1235 | os.remove('test.ini') 1236 | 1237 | 1238 | def test_interpolation_using_default_sections(): 1239 | c = ConfigObj() 1240 | c['DEFAULT'] = {'a' : 'fish'} 1241 | c['a'] = '%(a)s' 1242 | assert c.write() == ['a = %(a)s', '[DEFAULT]', 'a = fish'] 1243 | 1244 | 1245 | class TestIndentation(object): 1246 | @pytest.fixture 1247 | def max_tabbed_cfg(self): 1248 | return ['[sect]', ' [[sect]]', ' foo = bar'] 1249 | 1250 | def test_write_dictionary(self): 1251 | assert ConfigObj({'sect': {'sect': {'foo': 'bar'}}}).write() == [ 1252 | '[sect]', ' [[sect]]', ' foo = bar' 1253 | ] 1254 | 1255 | def test_indentation_preserved(self, max_tabbed_cfg): 1256 | for cfg_content in ( 1257 | ['[sect]', '[[sect]]', 'foo = bar'], 1258 | ['[sect]', ' [[sect]]', ' foo = bar'], 1259 | max_tabbed_cfg 1260 | ): 1261 | assert ConfigObj(cfg_content).write() == cfg_content 1262 | 1263 | def test_handle_tabs_vs_spaces(self, max_tabbed_cfg): 1264 | one_tab = ['[sect]', '\t[[sect]]', '\t\tfoo = bar'] 1265 | two_tabs = ['[sect]', '\t\t[[sect]]', '\t\t\t\tfoo = bar'] 1266 | tabs_and_spaces = [b'[sect]', b'\t \t [[sect]]', 1267 | b'\t \t \t \t foo = bar'] 1268 | 1269 | assert ConfigObj(one_tab).write() == one_tab 1270 | assert ConfigObj(two_tabs).write() == two_tabs 1271 | assert ConfigObj(tabs_and_spaces).write() == [s.decode('utf-8') for s in tabs_and_spaces] 1272 | assert ConfigObj(max_tabbed_cfg, indent_type=chr(9)).write() == one_tab 1273 | assert ConfigObj(one_tab, indent_type=' ').write() == max_tabbed_cfg 1274 | 1275 | 1276 | class TestEdgeCasesWhenWritingOut(object): 1277 | def test_newline_terminated(self, empty_cfg): 1278 | empty_cfg.newlines = '\n' 1279 | empty_cfg['a'] = 'b' 1280 | collector = io.BytesIO() 1281 | empty_cfg.write(collector) 1282 | assert collector.getvalue() == b'a = b\n' 1283 | 1284 | def test_hash_escaping(self, empty_cfg): 1285 | empty_cfg.newlines = '\n' 1286 | empty_cfg['#a'] = 'b # something' 1287 | collector = io.BytesIO() 1288 | empty_cfg.write(collector) 1289 | assert collector.getvalue() == b'"#a" = "b # something"\n' 1290 | 1291 | empty_cfg = ConfigObj() 1292 | empty_cfg.newlines = '\n' 1293 | empty_cfg['a'] = 'b # something', 'c # something' 1294 | collector = io.BytesIO() 1295 | empty_cfg.write(collector) 1296 | assert collector.getvalue() == b'a = "b # something", "c # something"\n' 1297 | 1298 | def test_detecting_line_endings_from_existing_files(self): 1299 | for expected_line_ending in ('\r\n', '\n'): 1300 | with open('temp', 'w') as h: 1301 | h.write(expected_line_ending) 1302 | c = ConfigObj('temp') 1303 | assert c.newlines == expected_line_ending 1304 | os.remove('temp') 1305 | 1306 | def test_writing_out_dict_value_with_unrepr(self): 1307 | # issue #42 1308 | cfg = [str('thing = {"a": 1}')] 1309 | c = ConfigObj(cfg, unrepr=True) 1310 | assert repr(c) == "ConfigObj({'thing': {'a': 1}})" 1311 | assert c.write() == ["thing = {'a': 1}"] 1312 | -------------------------------------------------------------------------------- /src/tests/test_validate.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from configobj import ConfigObj 4 | import pytest 5 | from configobj.validate import Validator, VdtValueTooSmallError 6 | 7 | 8 | class TestImporting(object): 9 | def test_top_level(self, val): 10 | import validate 11 | assert val.__class__ is validate.Validator 12 | 13 | def test_within_configobj_using_from(self, val): 14 | from configobj import validate 15 | assert val.__class__ is validate.Validator 16 | 17 | def test_within_configobj(self, val): 18 | import configobj.validate 19 | assert val.__class__ is configobj.validate.Validator 20 | 21 | 22 | class TestBasic(object): 23 | def test_values_too_small(self, val): 24 | config = ''' 25 | test1=40 26 | test2=hello 27 | test3=3 28 | test4=5.0 29 | [section] 30 | test1=40 31 | test2=hello 32 | test3=3 33 | test4=5.0 34 | [[sub section]] 35 | test1=40 36 | test2=hello 37 | test3=3 38 | test4=5.0 39 | '''.splitlines() 40 | configspec = ''' 41 | test1= integer(30,50) 42 | test2= string 43 | test3=integer 44 | test4=float(6.0) 45 | [section ] 46 | test1=integer(30,50) 47 | test2=string 48 | test3=integer 49 | test4=float(6.0) 50 | [[sub section]] 51 | test1=integer(30,50) 52 | test2=string 53 | test3=integer 54 | test4=float(6.0) 55 | '''.splitlines() 56 | c1 = ConfigObj(config, configspec=configspec) 57 | test = c1.validate(val) 58 | assert test == { 59 | 'test1': True, 60 | 'test2': True, 61 | 'test3': True, 62 | 'test4': False, 63 | 'section': { 64 | 'test1': True, 65 | 'test2': True, 66 | 'test3': True, 67 | 'test4': False, 68 | 'sub section': { 69 | 'test1': True, 70 | 'test2': True, 71 | 'test3': True, 72 | 'test4': False, 73 | }, 74 | }, 75 | } 76 | 77 | with pytest.raises(VdtValueTooSmallError) as excinfo: 78 | val.check(c1.configspec['test4'], c1['test4']) 79 | assert str(excinfo.value) == 'the value "5.0" is too small.' 80 | 81 | def test_values(self, val): 82 | val_test_config = ''' 83 | key = 0 84 | key2 = 1.1 85 | [section] 86 | key = some text 87 | key2 = 1.1, 3.0, 17, 6.8 88 | [[sub-section]] 89 | key = option1 90 | key2 = True'''.splitlines() 91 | val_test_configspec = ''' 92 | key = integer 93 | key2 = float 94 | [section] 95 | key = string 96 | key2 = float_list(4) 97 | [[sub-section]] 98 | key = option(option1, option2) 99 | key2 = boolean'''.splitlines() 100 | val_test = ConfigObj(val_test_config, configspec=val_test_configspec) 101 | assert val_test.validate(val) 102 | val_test['key'] = 'text not a digit' 103 | val_res = val_test.validate(val) 104 | assert val_res == {'key2': True, 'section': True, 'key': False} 105 | 106 | def test_defaults(self, val): 107 | configspec = ''' 108 | test1=integer(30,50, default=40) 109 | test2=string(default="hello") 110 | test3=integer(default=3) 111 | test4=float(6.0, default=6.0) 112 | [section ] 113 | test1=integer(30,50, default=40) 114 | test2=string(default="hello") 115 | test3=integer(default=3) 116 | test4=float(6.0, default=6.0) 117 | [[sub section]] 118 | test1=integer(30,50, default=40) 119 | test2=string(default="hello") 120 | test3=integer(default=3) 121 | test4=float(6.0, default=6.0) 122 | '''.splitlines() 123 | default_test = ConfigObj(['test1=30'], configspec=configspec) 124 | assert repr(default_test) == "ConfigObj({'test1': '30'})" 125 | assert default_test.defaults == [] 126 | assert default_test.default_values == {} 127 | assert default_test.validate(val) 128 | assert default_test == { 129 | 'test1': 30, 130 | 'test2': 'hello', 131 | 'test3': 3, 132 | 'test4': 6.0, 133 | 'section': { 134 | 'test1': 40, 135 | 'test2': 'hello', 136 | 'test3': 3, 137 | 'test4': 6.0, 138 | 'sub section': { 139 | 'test1': 40, 140 | 'test3': 3, 141 | 'test2': 'hello', 142 | 'test4': 6.0, 143 | }, 144 | }, 145 | } 146 | 147 | assert default_test.defaults == ['test2', 'test3', 'test4'] 148 | assert default_test.default_values == { 149 | 'test1': 40, 'test2': 'hello', 150 | 'test3': 3, 'test4': 6.0 151 | } 152 | assert default_test.restore_default('test1') == 40 153 | assert default_test['test1'] == 40 154 | assert 'test1' in default_test.defaults 155 | 156 | def change(section, key): 157 | section[key] = 3 158 | default_test.walk(change) 159 | assert default_test['section']['sub section']['test4'] == 3 160 | 161 | default_test.restore_defaults() 162 | assert default_test == { 163 | 'test1': 40, 164 | 'test2': "hello", 165 | 'test3': 3, 166 | 'test4': 6.0, 167 | 'section': { 168 | 'test1': 40, 169 | 'test2': "hello", 170 | 'test3': 3, 171 | 'test4': 6.0, 172 | 'sub section': { 173 | 'test1': 40, 174 | 'test2': "hello", 175 | 'test3': 3, 176 | 'test4': 6.0 177 | }}} 178 | -------------------------------------------------------------------------------- /src/tests/test_validate_errors.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from configobj import ConfigObj, get_extra_values, ParseError, NestingError 6 | from configobj.validate import Validator, VdtUnknownCheckError 7 | 8 | @pytest.fixture() 9 | def thisdir(): 10 | return os.path.dirname(os.path.join(os.getcwd(), __file__)) 11 | 12 | 13 | @pytest.fixture() 14 | def inipath(thisdir): 15 | return os.path.join(thisdir, 'conf.ini') 16 | 17 | 18 | @pytest.fixture() 19 | def specpath(thisdir): 20 | return os.path.join(thisdir, 'conf.spec') 21 | 22 | 23 | @pytest.fixture() 24 | def conf(inipath, specpath): 25 | return ConfigObj(inipath, configspec=specpath) 26 | 27 | 28 | def test_validate_no_valid_entries(conf): 29 | validator = Validator() 30 | result = conf.validate(validator) 31 | assert not result 32 | 33 | 34 | def test_validate_preserve_errors(conf): 35 | validator = Validator() 36 | result = conf.validate(validator, preserve_errors=True) 37 | 38 | assert not result['value'] 39 | assert not result['missing-section'] 40 | 41 | section = result['section'] 42 | assert not section['value'] 43 | assert not section['sub-section']['value'] 44 | assert not section['missing-subsection'] 45 | 46 | 47 | def test_validate_extra_values(conf): 48 | conf.validate(Validator(), preserve_errors=True) 49 | 50 | assert conf.extra_values == ['extra', 'extra-section'] 51 | assert conf['section'].extra_values == ['extra-sub-section'] 52 | assert conf['section']['sub-section'].extra_values == ['extra'] 53 | 54 | 55 | def test_get_extra_values(conf): 56 | conf.validate(Validator(), preserve_errors=True) 57 | extra_values = get_extra_values(conf) 58 | 59 | expected = sorted([ 60 | ((), 'extra'), 61 | ((), 'extra-section'), 62 | (('section', 'sub-section'), 'extra'), 63 | (('section',), 'extra-sub-section'), 64 | ]) 65 | assert sorted(extra_values) == expected 66 | 67 | 68 | def test_invalid_lines_with_percents(tmpdir, specpath): 69 | ini = tmpdir.join('config.ini') 70 | ini.write('extra: %H:%M\n') 71 | with pytest.raises(ParseError): 72 | conf = ConfigObj(str(ini), configspec=specpath, file_error=True) 73 | 74 | 75 | def test_no_parent(tmpdir, specpath): 76 | ini = tmpdir.join('config.ini') 77 | ini.write('[[haha]]') 78 | with pytest.raises(NestingError): 79 | conf = ConfigObj(str(ini), configspec=specpath, file_error=True) 80 | 81 | 82 | def test_re_dos(val): 83 | value = "aaa" 84 | i = 165100 85 | attack = '\x00'*i + ')' + '('*i 86 | with pytest.raises(VdtUnknownCheckError): 87 | val.check(attack, value) 88 | -------------------------------------------------------------------------------- /src/validate/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a backwards compatibility-shim to support: 3 | 4 | ``` 5 | import validate 6 | ``` 7 | 8 | in a future release, we'd expect this to no longer work and 9 | instead using: 10 | 11 | ``` 12 | import configobj.validate 13 | ``` 14 | 15 | or: 16 | 17 | ``` 18 | from configobj import validate 19 | ``` 20 | """ 21 | from configobj.validate import * 22 | 23 | --------------------------------------------------------------------------------