diff options
author | Ludovico Cavedon <ludovico.cavedon@gmail.com> | 2009-10-20 23:52:30 -0700 |
---|---|---|
committer | Ludovico Cavedon <ludovico.cavedon@gmail.com> | 2009-10-20 23:52:30 -0700 |
commit | 8cbb16c4830d341deb19d77d8ff9c10813460e46 (patch) | |
tree | cf55736336137013468fc6a4a3add54e74f45f74 |
Imported Upstream version 0.3.1
-rw-r--r-- | Changelog | 85 | ||||
-rw-r--r-- | LICENSE | 30 | ||||
-rw-r--r-- | LICENSE-PSF | 262 | ||||
-rw-r--r-- | MANIFEST.in | 7 | ||||
-rw-r--r-- | Makefile | 19 | ||||
-rw-r--r-- | PKG-INFO | 24 | ||||
-rw-r--r-- | README | 28 | ||||
-rwxr-xr-x | html/index.html | 170 | ||||
-rw-r--r-- | html/style.css | 123 | ||||
-rw-r--r-- | iniparse/__init__.py | 13 | ||||
-rw-r--r-- | iniparse/compat.py | 351 | ||||
-rw-r--r-- | iniparse/config.py | 273 | ||||
-rw-r--r-- | iniparse/ini.py | 630 | ||||
-rw-r--r-- | python-iniparse.spec | 72 | ||||
-rwxr-xr-x | runtests.py | 12 | ||||
-rw-r--r-- | setup.py | 41 | ||||
-rw-r--r-- | tests/__init__.py | 21 | ||||
-rw-r--r-- | tests/test_compat.py | 497 | ||||
-rw-r--r-- | tests/test_fuzz.py | 111 | ||||
-rw-r--r-- | tests/test_ini.py | 391 | ||||
-rw-r--r-- | tests/test_misc.py | 411 | ||||
-rw-r--r-- | tests/test_unicode.py | 48 |
22 files changed, 3619 insertions, 0 deletions
diff --git a/Changelog b/Changelog new file mode 100644 index 0000000..daf1bd1 --- /dev/null +++ b/Changelog @@ -0,0 +1,85 @@ +2009-03-02 +* released 0.3.1 + +2009-03-01 +* Fix empty line bugs introduced by the compatibility hack + +2009-02-27 +* released 0.3.0 + +2009-02-22 +* Make INIConfig objects pickle-able +* Fix the defaults() method +* Replicate ConfigParser behavior regarding empty lines in + multi-line values - empty lines are stripped on parsing, + but preserved when the value is explicitly set. + +2009-02-10 +* Skip DEFAULT section when listing sections (issue 8) + +2009-02-03 +* Bugfixes for continuation line behavior, based on patch by + sidnei.da.silva - (1) preserve empty lines in multi-line + values, and (2) fix assignment to multi-line values + +2008-12-06 +* released 0.2.4 + +2008-12-06 +* upgraded test_compat to the tests included with python-2.6.1 +* fixed compatibility warnings generated by the '-3' option +* Python's ConfigParser has acquired the ability to use custom dict + types - presumably to support user-controlled ordering. This + feature does not seem to make sense in the context of iniparse, + so I'm not planning on adding support for it (unless I hear + otherwise from users). + +2008-12-05 +* add hack to fix unicode support on python-2.4.x +* use the built-in set() type instead of the pre-2.4 sets module + +2008-04-06 +* support files opened in unicode mode +* handle BOMs in unicode mode + +2008-03-30 +* cleanup ConfigNamespace docs +* rename readfp() to _readfp() +* replace import_namespace() with an update_config() utility function. + +2007-12-11 +* released 0.2.3 + +2007-12-09 +* preserve whitespace around '=' and ':' + +2007-10-02 +* handle empty files + +2007-09-24 +* released 0.2.2 + +2007-09-09 +* allow multi-line values to span comments and blank lines + +2007-08-07 +* released version 0.2.1 + +2007-07-28 +* only use .readline() on file objects for better ConfigParser compatibility +* spec file fixes for fedora + +2007-07-19 +* released version 0.2 + +2007-07-10 +* renamed project to iniparse +* renamed classes to reflect new project name +* made names more friendly and PEP 8 compliant + +2007-07-10 +* imported into google-code + +2004-10-03 +* packaged, added licences, etc. +* released version 0.1 @@ -0,0 +1,30 @@ +Copyright (c) 2001, 2002, 2003 Python Software Foundation +Copyright (c) 2004-2008 Paramjit Oberoi <param.cs.wisc.edu> +Copyright (c) 2007 Tim Lauridsen <tla@rasmil.dk> +All Rights Reserved. + +iniparse/compat.py and tests/test_compat.py contain code derived from +lib/python-2.3/ConfigParser.py and lib/python-2.3/test/test_cfgparse.py +respectively. Other code may contain small snippets from those two files +as well. The Python license (LICENSE-PSF) applies to that code. + +--------------------------------------------------------------------------- +The MIT License + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/LICENSE-PSF b/LICENSE-PSF new file mode 100644 index 0000000..88d2f19 --- /dev/null +++ b/LICENSE-PSF @@ -0,0 +1,262 @@ +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations (now Zope +Corporation, see http://www.zope.com). In 2001, the Python Software +Foundation (PSF, see http://www.python.org/psf/) was formed, a +non-profit organization created specifically to own Python-related +Intellectual Property. Zope Corporation is a sponsoring member of +the PSF. + +All Python releases are Open Source (see http://www.opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.2 2.1.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2.1 2.2 2002 PSF yes + 2.2.2 2.2.1 2002 PSF yes + 2.2.3 2.2.2 2003 PSF yes + 2.3 2.2.2 2002-2003 PSF yes + 2.3.1 2.3 2002-2003 PSF yes + 2.3.2 2.3.1 2002-2003 PSF yes + 2.3.3 2.3.2 2002-2003 PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +PSF LICENSE AGREEMENT FOR PYTHON 2.3 +------------------------------------ + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using Python 2.3 software in source or binary form and its +associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 2.3 +alone or in any derivative version, provided, however, that PSF's +License Agreement and PSF's notice of copyright, i.e., "Copyright (c) +2001, 2002, 2003 Python Software Foundation; All Rights Reserved" are +retained in Python 2.3 alone or in any derivative version prepared by +Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 2.3 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 2.3. + +4. PSF is making Python 2.3 available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 2.3 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +2.3 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 2.3, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python 2.3, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..52f125d --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +include iniparse/*.py +include tests/*.py +include html/*.html +include html/style.css +include Makefile runtests.py +include MANIFEST.in setup.py python-iniparse.spec +include README Changelog LICENSE LICENSE-PSF diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0e7631a --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +PKGNAME = iniparse +SPECVERSION=$(shell awk '/Version:/ { print $$2 }' python-${PKGNAME}.spec) +SETUPVERSION=$(shell awk -F\' '/VERSION =/ { print $$2 }' setup.py) + +clean: + rm -f *.pyc *.pyo *~ *.bak + rm -f iniparse/*.pyc iniparse/*.pyo iniparse/*~ iniparse/*.bak + rm -f tests/*.pyc tests/*.pyo tests/*~ tests/*.bak + rm -f *.tar.gz MANIFEST + +archive: + python setup.py sdist -d . + @echo "The archive is in ${PKGNAME}-$(SETUPVERSION).tar.gz" + +buildrpm: archive + rpmbuild -ta ${PKGNAME}-$(SPECVERSION).tar.gz + +pychecker: + pychecker --stdlib iniparse/*py tests/*py diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..1d3a599 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,24 @@ +Metadata-Version: 1.0 +Name: iniparse +Version: 0.3.1 +Summary: Accessing and Modifying INI files +Home-page: http://code.google.com/p/iniparse/ +Author: Paramjit Oberoi +Author-email: param@cs.wisc.edu +License: MIT +Description: iniparse is an INI parser for Python which is API compatible + with the standard library's ConfigParser, preserves structure of INI + files (order of sections & options, indentation, comments, and blank + lines are preserved when data is updated), and is more convenient to + use. +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: License :: OSI Approved :: Python Software Foundation License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.4 +Classifier: Programming Language :: Python :: 2.5 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Topic :: Software Development :: Libraries :: Python Modules @@ -0,0 +1,28 @@ +Introduction to iniparse + +iniparse is a INI parser for Python which is: + +* Compatible with ConfigParser: Backward compatible implementations + of ConfigParser, RawConfigParser, and SafeConfigParser are included + that are API-compatible with the Python standard library. + +* Preserves structure of INI files: Order of sections & options, + indentation, comments, and blank lines are preserved as far as + possible when data is updated. + +* More convenient: Values can be accessed using dotted notation + (cfg.user.name), or using container syntax (cfg['user']['name']). + +It is very useful for config files that are updated both by users and by +programs, since it is very disorienting for a user to have her config file +completely rearranged whenever a program changes it. iniparse also allows +making the order of entries in a config file significant, which is desirable +in applications like image galleries. + +Website: http://code.google.com/p/iniparse/ +Mailing List: iniparse-discuss@googlegroups.com + +Copyright (c) 2001-2008 Python Software Foundation +Copyright (c) 2004-2009 Paramjit Oberoi <param.cs.wisc.edu> +Copyright (c) 2007 Tim Lauridsen <tla@rasmil.dk> +All Rights Reserved. See LICENSE-PSF & LICENSE for details. diff --git a/html/index.html b/html/index.html new file mode 100755 index 0000000..73adc9e --- /dev/null +++ b/html/index.html @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" +"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html> + <head> + <title>iniparse</title> + <style type="text/css" title="stylesheet"> @import "style.css"; </style> + <link rel="SHORTCUT ICON" href="http://www.python.org/pics/pyfav.gif" /> + </head> + + <body> + <div id="title"> + <h1 style="font-family: monospace">iniparse</h1> + <p>Better INI parser for Python</p> + </div> + + <div class="box"> + <div class="boxtitle">Introduction</div> + + <div class="boxitem"> + <p><code>iniparse</code> is a INI parser for + <a href="http://www.python.org/">Python</a> + which is:</p> + + <ul> + + <li><b>Compatible with <code> + <a href="http://docs.python.org/lib/module-ConfigParser.html">ConfigParser</a></code></b>: + Backward compatible implementations of <code>ConfigParser</code>, + <code>RawConfigParser</code>, and <code>SafeConfigParser</code> + are included that are API-compatible with the Python standard + library.</li> + + <li><b>Preserves structure of INI files</b>: Order of sections & + options, indentation, comments, and blank lines are preserved as far + as possible when data is updated.</li> + + <li><b>More convenient</b>: Values can be accessed using dotted + notation (<code>cfg.user.name</code>), or using container syntax + (<code>cfg['user']['name']</code>).</li> + + <li><b>Extensible</b>: It is possible to add other configuration + formats, and to convert between different formats (as long as the + data models are compatible).</li> + + </ul> + + <p>It is very useful for config files that are updated both by users + and by programs, since it is very disorienting for a user to have + her config file completely rearranged whenever a program changes it. + iniparse also allows making the order of entries in a config file + significant, which is desirable in applications like image + galleries.</p> + + <p><b>Website</b>: <a href="http://code.google.com/p/iniparse/" + >http://code.google.com/p/iniparse/</a></p> + + </div> + </div> + + <div class="box"> + <div class="boxtitle">Examples</div> + + <div class="boxitem"> + <b>New API:</b> + <ul> + <li>Open an INI file: +<pre> +>>> from iniparse import INIConfig +>>> cfg = INIConfig(file('options.ini')) +</pre> + </li> + <li>Access/Modify data: +<pre> +>>> print cfg.playlist.expand_playlist +True +>>> print cfg.ui.width +150 +>>> cfg.ui.width = 200 +>>> print cfg['ui']['width'] +200 +</pre> + </li> + <li>Print data: +<pre> +>>> print cfg +[playlist] +expand_playlist = True + +[ui] +display_clock = True +display_qlength = True +width = 200 +</pre> + </li> + </ul> + + <b>Backward Compatible API:</b> + <ul> + <li>The entire ConfigParser API is supported. This is just a brief + example: +<pre> +>>> from iniparse import ConfigParser +>>> cfgpr = ConfigParser() +>>> cfgpr.read('options.ini') +>>> print cfgpr.get('ui', 'width') +150 +>>> cfgpr.set('ui', 'width', 175) +</pre> + </li> + <li>The new API can also be accessed via backward-compatible objects: +<pre> +>>> print cfgpr.data.playlist.expand_playlist +True +>>> cfgpr.data.ui.width = 200 +>>> print cfgpr.data.ui.width +200 +</pre> + </li> + </ul> + + <b>A non-INI example:</b> + <ul> + <li>A simple dotted format is also implemented: +<pre> +>>> from iniparse import BasicConfig +>>> n = BasicConfig() +>>> n.x = 7 +>>> n.name.first = 'paramjit' +>>> n.name.last = 'oberoi' +>>> print n.x +7 +>>> print n.name.first +'paramjit' +>>> print n +name.first = paramjit +name.last = oberoi +x = 7 +</pre> + </li> + <li>Convert to INI: +<pre> +>>> from iniparse import INIConfig +>>> i = INIConfig() +>>> del n.x # since INI doesn't support top-level values +>>> i.import_config(n) +>>> print i +[name] +first = paramjit +last = oberoi +</pre> + </li> + </ul> + + </div> + + <!-- div class="boxitem"> + <p>For more information, see the automatically generated + <a href="iniparse.html">API documentation</a>.</p> + </div --> + + </div> + <div id="footer"> + <p> + Updated on 15 July 2007 + </p> + </div> + </body> +</html> diff --git a/html/style.css b/html/style.css new file mode 100644 index 0000000..378b92a --- /dev/null +++ b/html/style.css @@ -0,0 +1,123 @@ +/* All colors are at the beginning */ +/* Colors are based on a "My Yahoo" theme */ + +body, #title, #title A:link, #title A:visited, #title a:hover +{ + color: black; + background: #a0b8c8; +} + +.box { + color: black; + background: #ffffcc; +} + +.boxtitle { + color: black; + background: #dcdcdc; +} + +#menu { + color: black; + background: #dcdcdc; +} + +/* Colors finished; now for the layout */ + +body { + margin-left: 10%; + margin-right: 10%; + font-family: sans-serif; + font-size: 0.8em; +} + +#title { + text-align: center; +} + +#title h1 { + margin-bottom: .3em; + padding-bottom: .2em; + font-size: 1.8em; + letter-spacing: .2em; + border-bottom: 1px solid; +} + +#title p { + letter-spacing: .2em; + margin-top: .1em; + margin-bottom: .1em; +} + +#title A:link, #title A:visited { + text-decoration: none; +} + +#title A:hover { + text-decoration: underline; +} + +#menu { + margin-top: 2em; + padding: 0.3em; + text-align: center; + font-weight: bold; + border: 1px solid; +} + +#footer { + margin-top: 1em; + padding-right: 0.2em; + padding-left: 0.2em; + font-style: italic; + text-align: center; +} + +#footer p { + margin: 0.5em; +} + +.box { + margin-top: 2em; + padding: 0em; + border: 1px solid; +} + +.boxtitle { + margin: 0; + padding-bottom: 0.1em; + padding-top: 0.2em; + text-align: center; + font-size: 1.3em; + font-weight: bold; + border-bottom: 1px solid; +} + +.boxitem { + margin: 1em; +} + +ul { + margin-top: 0.2em; + margin-bottom: 0; +} + +li { + margin-bottom: 0.1em; +} + +.leftfloat { + width: 50%; + float: left; +} + +.rightfloat { + width: 50%; + float: right; +} + +.clear { + clear: both; + width: 0; + height: 0; +} diff --git a/iniparse/__init__.py b/iniparse/__init__.py new file mode 100644 index 0000000..350b7a7 --- /dev/null +++ b/iniparse/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2001, 2002, 2003 Python Software Foundation +# Copyright (c) 2004-2008 Paramjit Oberoi <param.cs.wisc.edu> +# Copyright (c) 2007 Tim Lauridsen <tla@rasmil.dk> +# All Rights Reserved. See LICENSE-PSF & LICENSE for details. + +from ini import INIConfig +from config import BasicConfig, ConfigNamespace +from compat import RawConfigParser, ConfigParser, SafeConfigParser + +__all__ = [ + 'INIConfig', 'BasicConfig', 'ConfigNamespace', + 'RawConfigParser', 'ConfigParser', 'SafeConfigParser', +] diff --git a/iniparse/compat.py b/iniparse/compat.py new file mode 100644 index 0000000..3de6148 --- /dev/null +++ b/iniparse/compat.py @@ -0,0 +1,351 @@ +# Copyright (c) 2001, 2002, 2003 Python Software Foundation +# Copyright (c) 2004-2008 Paramjit Oberoi <param.cs.wisc.edu> +# All Rights Reserved. See LICENSE-PSF & LICENSE for details. + +"""Compatibility interfaces for ConfigParser + +Interfaces of ConfigParser, RawConfigParser and SafeConfigParser +should be completely identical to the Python standard library +versions. Tested with the unit tests included with Python-2.3.4 + +The underlying INIConfig object can be accessed as cfg.data +""" + +import re +from ConfigParser import DuplicateSectionError, \ + NoSectionError, NoOptionError, \ + InterpolationMissingOptionError, \ + InterpolationDepthError, \ + InterpolationSyntaxError, \ + DEFAULTSECT, MAX_INTERPOLATION_DEPTH + +# These are imported only for compatiability. +# The code below does not reference them directly. +from ConfigParser import Error, InterpolationError, \ + MissingSectionHeaderError, ParsingError + +import ini + +class RawConfigParser(object): + def __init__(self, defaults=None, dict_type=dict): + if dict_type != dict: + raise ValueError('Custom dict types not supported') + self.data = ini.INIConfig(defaults=defaults, optionxformsource=self) + + def optionxform(self, optionstr): + return optionstr.lower() + + def defaults(self): + d = {} + secobj = self.data._defaults + for name in secobj._options: + d[name] = secobj._compat_get(name) + return d + + def sections(self): + """Return a list of section names, excluding [DEFAULT]""" + return list(self.data) + + def add_section(self, section): + """Create a new section in the configuration. + + Raise DuplicateSectionError if a section by the specified name + already exists. Raise ValueError if name is DEFAULT or any of + its case-insensitive variants. + """ + # The default section is the only one that gets the case-insensitive + # treatment - so it is special-cased here. + if section.lower() == "default": + raise ValueError, 'Invalid section name: %s' % section + + if self.has_section(section): + raise DuplicateSectionError(section) + else: + self.data._new_namespace(section) + + def has_section(self, section): + """Indicate whether the named section is present in the configuration. + + The DEFAULT section is not acknowledged. + """ + try: + self.data[section] + return True + except KeyError: + return False + + def options(self, section): + """Return a list of option names for the given section name.""" + try: + return list(self.data[section]) + except KeyError: + raise NoSectionError(section) + + def read(self, filenames): + """Read and parse a filename or a list of filenames. + + Files that cannot be opened are silently ignored; this is + designed so that you can specify a list of potential + configuration file locations (e.g. current directory, user's + home directory, systemwide directory), and all existing + configuration files in the list will be read. A single + filename may also be given. + """ + files_read = [] + if isinstance(filenames, basestring): + filenames = [filenames] + for filename in filenames: + try: + fp = open(filename) + except IOError: + continue + files_read.append(filename) + self.data._readfp(fp) + fp.close() + return files_read + + def readfp(self, fp, filename=None): + """Like read() but the argument must be a file-like object. + + The `fp' argument must have a `readline' method. Optional + second argument is the `filename', which if not given, is + taken from fp.name. If fp has no `name' attribute, `<???>' is + used. + """ + self.data._readfp(fp) + + def get(self, section, option, vars=None): + if not self.has_section(section): + raise NoSectionError(section) + if vars is not None and option in vars: + value = vars[option] + try: + sec = self.data[section] + return sec._compat_get(option) + except KeyError: + raise NoOptionError(option, section) + + def items(self, section): + try: + ans = [] + for opt in self.data[section]: + ans.append((opt, self.get(section, opt))) + return ans + except KeyError: + raise NoSectionError(section) + + def getint(self, section, option): + return int(self.get(section, option)) + + def getfloat(self, section, option): + return float(self.get(section, option)) + + _boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True, + '0': False, 'no': False, 'false': False, 'off': False} + + def getboolean(self, section, option): + v = self.get(section, option) + if v.lower() not in self._boolean_states: + raise ValueError, 'Not a boolean: %s' % v + return self._boolean_states[v.lower()] + + def has_option(self, section, option): + """Check for the existence of a given option in a given section.""" + try: + sec = self.data[section] + except KeyError: + raise NoSectionError(section) + try: + sec[option] + return True + except KeyError: + return False + + def set(self, section, option, value): + """Set an option.""" + try: + self.data[section][option] = value + except KeyError: + raise NoSectionError(section) + + def write(self, fp): + """Write an .ini-format representation of the configuration state.""" + fp.write(str(self.data)) + + def remove_option(self, section, option): + """Remove an option.""" + try: + sec = self.data[section] + except KeyError: + raise NoSectionError(section) + try: + sec[option] + del sec[option] + return 1 + except KeyError: + return 0 + + def remove_section(self, section): + """Remove a file section.""" + if not self.has_section(section): + return False + del self.data[section] + return True + + +class ConfigDict(object): + """Present a dict interface to a ini section.""" + + def __init__(self, cfg, section, vars): + self.cfg = cfg + self.section = section + self.vars = vars + + def __getitem__(self, key): + try: + return RawConfigParser.get(self.cfg, self.section, key, self.vars) + except (NoOptionError, NoSectionError): + raise KeyError(key) + + +class ConfigParser(RawConfigParser): + + def get(self, section, option, raw=False, vars=None): + """Get an option value for a given section. + + All % interpolations are expanded in the return values, based on the + defaults passed into the constructor, unless the optional argument + `raw' is true. Additional substitutions may be provided using the + `vars' argument, which must be a dictionary whose contents overrides + any pre-existing defaults. + + The section DEFAULT is special. + """ + if section != DEFAULTSECT and not self.has_section(section): + raise NoSectionError(section) + + option = self.optionxform(option) + value = RawConfigParser.get(self, section, option, vars) + + if raw: + return value + else: + d = ConfigDict(self, section, vars) + return self._interpolate(section, option, value, d) + + def _interpolate(self, section, option, rawval, vars): + # do the string interpolation + value = rawval + depth = MAX_INTERPOLATION_DEPTH + while depth: # Loop through this until it's done + depth -= 1 + if "%(" in value: + try: + value = value % vars + except KeyError, e: + raise InterpolationMissingOptionError( + option, section, rawval, e.args[0]) + else: + break + if value.find("%(") != -1: + raise InterpolationDepthError(option, section, rawval) + return value + + def items(self, section, raw=False, vars=None): + """Return a list of tuples with (name, value) for each option + in the section. + + All % interpolations are expanded in the return values, based on the + defaults passed into the constructor, unless the optional argument + `raw' is true. Additional substitutions may be provided using the + `vars' argument, which must be a dictionary whose contents overrides + any pre-existing defaults. + + The section DEFAULT is special. + """ + if section != DEFAULTSECT and not self.has_section(section): + raise NoSectionError(section) + if vars is None: + options = list(self.data[section]) + else: + options = [] + for x in self.data[section]: + if x not in vars: + options.append(x) + options.extend(vars.keys()) + + if "__name__" in options: + options.remove("__name__") + + d = ConfigDict(self, section, vars) + if raw: + return [(option, d[option]) + for option in options] + else: + return [(option, self._interpolate(section, option, d[option], d)) + for option in options] + + +class SafeConfigParser(ConfigParser): + _interpvar_re = re.compile(r"%\(([^)]+)\)s") + _badpercent_re = re.compile(r"%[^%]|%$") + + def set(self, section, option, value): + if not isinstance(value, basestring): + raise TypeError("option values must be strings") + # check for bad percent signs: + # first, replace all "good" interpolations + tmp_value = self._interpvar_re.sub('', value) + # then, check if there's a lone percent sign left + m = self._badpercent_re.search(tmp_value) + if m: + raise ValueError("invalid interpolation syntax in %r at " + "position %d" % (value, m.start())) + + ConfigParser.set(self, section, option, value) + + def _interpolate(self, section, option, rawval, vars): + # do the string interpolation + L = [] + self._interpolate_some(option, L, rawval, section, vars, 1) + return ''.join(L) + + _interpvar_match = re.compile(r"%\(([^)]+)\)s").match + + def _interpolate_some(self, option, accum, rest, section, map, depth): + if depth > MAX_INTERPOLATION_DEPTH: + raise InterpolationDepthError(option, section, rest) + while rest: + p = rest.find("%") + if p < 0: + accum.append(rest) + return + if p > 0: + accum.append(rest[:p]) + rest = rest[p:] + # p is no longer used + c = rest[1:2] + if c == "%": + accum.append("%") + rest = rest[2:] + elif c == "(": + m = self._interpvar_match(rest) + if m is None: + raise InterpolationSyntaxError(option, section, + "bad interpolation variable reference %r" % rest) + var = m.group(1) + rest = rest[m.end():] + try: + v = map[var] + except KeyError: + raise InterpolationMissingOptionError( + option, section, rest, var) + if "%" in v: + self._interpolate_some(option, accum, v, + section, map, depth + 1) + else: + accum.append(v) + else: + raise InterpolationSyntaxError( + option, section, + "'%' must be followed by '%' or '(', found: " + repr(rest)) diff --git a/iniparse/config.py b/iniparse/config.py new file mode 100644 index 0000000..508dac2 --- /dev/null +++ b/iniparse/config.py @@ -0,0 +1,273 @@ +class ConfigNamespace(object): + """Abstract class representing the interface of Config objects. + + A ConfigNamespace is a collection of names mapped to values, where + the values may be nested namespaces. Values can be accessed via + container notation - obj[key] - or via dotted notation - obj.key. + Both these access methods are equivalent. + + To minimize name conflicts between namespace keys and class members, + the number of class members should be minimized, and the names of + all class members should start with an underscore. + + Subclasses must implement the methods for container-like access, + and this class will automatically provide dotted access. + + """ + + # Methods that must be implemented by subclasses + + def __getitem__(self, key): + return NotImplementedError(key) + + def __setitem__(self, key, value): + raise NotImplementedError(key, value) + + def __delitem__(self, key): + raise NotImplementedError(key) + + def __iter__(self): + return NotImplementedError() + + def _new_namespace(self, name): + raise NotImplementedError(name) + + # Machinery for converting dotted access into contained access + # + # To distinguish between accesses of class members and namespace + # keys, we first call object.__getattribute__(). If that succeeds, + # the name is assumed to be a class member. Otherwise it is + # treated as a namespace key. + # + # Therefore, member variables should be defined in the class, + # not just in the __init__() function. See BasicNamespace for + # an example. + + def __getattr__(self, name): + try: + return self.__getitem__(name) + except KeyError: + return Undefined(name, self) + + def __setattr__(self, name, value): + try: + object.__getattribute__(self, name) + object.__setattr__(self, name, value) + except AttributeError: + self.__setitem__(name, value) + + def __delattr__(self, name): + try: + object.__getattribute__(self, name) + object.__delattr__(self, name) + except AttributeError: + self.__delitem__(name) + + def __getstate__(self): + return self.__dict__ + + def __setstate__(self, state): + self.__dict__.update(state) + +class Undefined(object): + """Helper class used to hold undefined names until assignment. + + This class helps create any undefined subsections when an + assignment is made to a nested value. For example, if the + statement is "cfg.a.b.c = 42", but "cfg.a.b" does not exist yet. + """ + + def __init__(self, name, namespace): + object.__setattr__(self, 'name', name) + object.__setattr__(self, 'namespace', namespace) + + def __setattr__(self, name, value): + obj = self.namespace._new_namespace(self.name) + obj[name] = value + + +# ---- Basic implementation of a ConfigNamespace + +class BasicConfig(ConfigNamespace): + """Represents a hierarchical collection of named values. + + Values are added using dotted notation: + + >>> n = BasicConfig() + >>> n.x = 7 + >>> n.name.first = 'paramjit' + >>> n.name.last = 'oberoi' + + ...and accessed the same way, or with [...]: + + >>> n.x + 7 + >>> n.name.first + 'paramjit' + >>> n.name.last + 'oberoi' + >>> n['x'] + 7 + >>> n['name']['first'] + 'paramjit' + + Iterating over the namespace object returns the keys: + + >>> l = list(n) + >>> l.sort() + >>> l + ['name', 'x'] + + Values can be deleted using 'del' and printed using 'print'. + + >>> n.aaa = 42 + >>> del n.x + >>> print n + aaa = 42 + name.first = paramjit + name.last = oberoi + + Nested namepsaces are also namespaces: + + >>> isinstance(n.name, ConfigNamespace) + True + >>> print n.name + first = paramjit + last = oberoi + >>> sorted(list(n.name)) + ['first', 'last'] + + Finally, values can be read from a file as follows: + + >>> from StringIO import StringIO + >>> sio = StringIO(''' + ... # comment + ... ui.height = 100 + ... ui.width = 150 + ... complexity = medium + ... have_python + ... data.secret.password = goodness=gracious me + ... ''') + >>> n = BasicConfig() + >>> n._readfp(sio) + >>> print n + complexity = medium + data.secret.password = goodness=gracious me + have_python + ui.height = 100 + ui.width = 150 + """ + + # this makes sure that __setattr__ knows this is not a namespace key + _data = None + + def __init__(self): + self._data = {} + + def __getitem__(self, key): + return self._data[key] + + def __setitem__(self, key, value): + self._data[key] = value + + def __delitem__(self, key): + del self._data[key] + + def __iter__(self): + return iter(self._data) + + def __str__(self, prefix=''): + lines = [] + keys = self._data.keys() + keys.sort() + for name in keys: + value = self._data[name] + if isinstance(value, ConfigNamespace): + lines.append(value.__str__(prefix='%s%s.' % (prefix,name))) + else: + if value is None: + lines.append('%s%s' % (prefix, name)) + else: + lines.append('%s%s = %s' % (prefix, name, value)) + return '\n'.join(lines) + + def _new_namespace(self, name): + obj = BasicConfig() + self._data[name] = obj + return obj + + def _readfp(self, fp): + while True: + line = fp.readline() + if not line: + break + + line = line.strip() + if not line: continue + if line[0] == '#': continue + data = line.split('=', 1) + if len(data) == 1: + name = line + value = None + else: + name = data[0].strip() + value = data[1].strip() + name_components = name.split('.') + ns = self + for n in name_components[:-1]: + try: + ns = ns[n] + if not isinstance(ns, ConfigNamespace): + raise TypeError('value-namespace conflict', n) + except KeyError: + ns = ns._new_namespace(n) + ns[name_components[-1]] = value + + +# ---- Utility functions + +def update_config(target, source): + """Imports values from source into target. + + Recursively walks the <source> ConfigNamespace and inserts values + into the <target> ConfigNamespace. For example: + + >>> n = BasicConfig() + >>> n.playlist.expand_playlist = True + >>> n.ui.display_clock = True + >>> n.ui.display_qlength = True + >>> n.ui.width = 150 + >>> print n + playlist.expand_playlist = True + ui.display_clock = True + ui.display_qlength = True + ui.width = 150 + + >>> from iniparse import ini + >>> i = ini.INIConfig() + >>> update_config(i, n) + >>> print i + [playlist] + expand_playlist = True + <BLANKLINE> + [ui] + display_clock = True + display_qlength = True + width = 150 + + """ + for name in source: + value = source[name] + if isinstance(value, ConfigNamespace): + try: + myns = target[name] + if not isinstance(myns, ConfigNamespace): + raise TypeError('value-namespace conflict') + except KeyError: + myns = target._new_namespace(name) + update_config(myns, value) + else: + target[name] = value + + + diff --git a/iniparse/ini.py b/iniparse/ini.py new file mode 100644 index 0000000..d58c38f --- /dev/null +++ b/iniparse/ini.py @@ -0,0 +1,630 @@ +"""Access and/or modify INI files + +* Compatiable with ConfigParser +* Preserves order of sections & options +* Preserves comments/blank lines/etc +* More conveninet access to data + +Example: + + >>> from StringIO import StringIO + >>> sio = StringIO('''# configure foo-application + ... [foo] + ... bar1 = qualia + ... bar2 = 1977 + ... [foo-ext] + ... special = 1''') + + >>> cfg = INIConfig(sio) + >>> print cfg.foo.bar1 + qualia + >>> print cfg['foo-ext'].special + 1 + >>> cfg.foo.newopt = 'hi!' + + >>> print cfg + # configure foo-application + [foo] + bar1 = qualia + bar2 = 1977 + newopt = hi! + [foo-ext] + special = 1 + +""" + +# An ini parser that supports ordered sections/options +# Also supports updates, while preserving structure +# Backward-compatiable with ConfigParser + +import re +from ConfigParser import DEFAULTSECT, ParsingError, MissingSectionHeaderError + +import config + +class LineType(object): + line = None + + def __init__(self, line=None): + if line is not None: + self.line = line.strip('\n') + + # Return the original line for unmodified objects + # Otherwise construct using the current attribute values + def __str__(self): + if self.line is not None: + return self.line + else: + return self.to_string() + + # If an attribute is modified after initialization + # set line to None since it is no longer accurate. + def __setattr__(self, name, value): + if hasattr(self,name): + self.__dict__['line'] = None + self.__dict__[name] = value + + def to_string(self): + raise Exception('This method must be overridden in derived classes') + + +class SectionLine(LineType): + regex = re.compile(r'^\[' + r'(?P<name>[^]]+)' + r'\]\s*' + r'((?P<csep>;|#)(?P<comment>.*))?$') + + def __init__(self, name, comment=None, comment_separator=None, + comment_offset=-1, line=None): + super(SectionLine, self).__init__(line) + self.name = name + self.comment = comment + self.comment_separator = comment_separator + self.comment_offset = comment_offset + + def to_string(self): + out = '[' + self.name + ']' + if self.comment is not None: + # try to preserve indentation of comments + out = (out+' ').ljust(self.comment_offset) + out = out + self.comment_separator + self.comment + return out + + def parse(cls, line): + m = cls.regex.match(line.rstrip()) + if m is None: + return None + return cls(m.group('name'), m.group('comment'), + m.group('csep'), m.start('csep'), + line) + parse = classmethod(parse) + + +class OptionLine(LineType): + def __init__(self, name, value, separator=' = ', comment=None, + comment_separator=None, comment_offset=-1, line=None): + super(OptionLine, self).__init__(line) + self.name = name + self.value = value + self.separator = separator + self.comment = comment + self.comment_separator = comment_separator + self.comment_offset = comment_offset + + def to_string(self): + out = '%s%s%s' % (self.name, self.separator, self.value) + if self.comment is not None: + # try to preserve indentation of comments + out = (out+' ').ljust(self.comment_offset) + out = out + self.comment_separator + self.comment + return out + + regex = re.compile(r'^(?P<name>[^:=\s[][^:=]*)' + r'(?P<sep>[:=]\s*)' + r'(?P<value>.*)$') + + def parse(cls, line): + m = cls.regex.match(line.rstrip()) + if m is None: + return None + + name = m.group('name').rstrip() + value = m.group('value') + sep = m.group('name')[len(name):] + m.group('sep') + + # comments are not detected in the regex because + # ensuring total compatibility with ConfigParser + # requires that: + # option = value ;comment // value=='value' + # option = value;1 ;comment // value=='value;1 ;comment' + # + # Doing this in a regex would be complicated. I + # think this is a bug. The whole issue of how to + # include ';' in the value needs to be addressed. + # Also, '#' doesn't mark comments in options... + + coff = value.find(';') + if coff != -1 and value[coff-1].isspace(): + comment = value[coff+1:] + csep = value[coff] + value = value[:coff].rstrip() + coff = m.start('value') + coff + else: + comment = None + csep = None + coff = -1 + + return cls(name, value, sep, comment, csep, coff, line) + parse = classmethod(parse) + + +class CommentLine(LineType): + regex = re.compile(r'^(?P<csep>[;#]|[rR][eE][mM])' + r'(?P<comment>.*)$') + + def __init__(self, comment='', separator='#', line=None): + super(CommentLine, self).__init__(line) + self.comment = comment + self.separator = separator + + def to_string(self): + return self.separator + self.comment + + def parse(cls, line): + m = cls.regex.match(line.rstrip()) + if m is None: + return None + return cls(m.group('comment'), m.group('csep'), line) + parse = classmethod(parse) + + +class EmptyLine(LineType): + # could make this a singleton + def to_string(self): + return '' + + value = property(lambda _: '') + + def parse(cls, line): + if line.strip(): return None + return cls(line) + parse = classmethod(parse) + + +class ContinuationLine(LineType): + regex = re.compile(r'^\s+(?P<value>.*)$') + + def __init__(self, value, value_offset=None, line=None): + super(ContinuationLine, self).__init__(line) + self.value = value + if value_offset is None: + value_offset = 8 + self.value_offset = value_offset + + def to_string(self): + return ' '*self.value_offset + self.value + + def parse(cls, line): + m = cls.regex.match(line.rstrip()) + if m is None: + return None + return cls(m.group('value'), m.start('value'), line) + parse = classmethod(parse) + + +class LineContainer(object): + def __init__(self, d=None): + self.contents = [] + self.orgvalue = None + if d: + if isinstance(d, list): self.extend(d) + else: self.add(d) + + def add(self, x): + self.contents.append(x) + + def extend(self, x): + for i in x: self.add(i) + + def get_name(self): + return self.contents[0].name + + def set_name(self, data): + self.contents[0].name = data + + def get_value(self): + if self.orgvalue is not None: + return self.orgvalue + elif len(self.contents) == 1: + return self.contents[0].value + else: + return '\n'.join([('%s' % x.value) for x in self.contents + if not isinstance(x, CommentLine)]) + + def set_value(self, data): + self.orgvalue = data + lines = ('%s' % data).split('\n') + + # If there is an existing ContinuationLine, use its offset + value_offset = None + for v in self.contents: + if isinstance(v, ContinuationLine): + value_offset = v.value_offset + break + + # Rebuild contents list, preserving initial OptionLine + self.contents = self.contents[0:1] + self.contents[0].value = lines[0] + del lines[0] + for line in lines: + if line.strip(): + self.add(ContinuationLine(line, value_offset)) + else: + self.add(EmptyLine()) + + name = property(get_name, set_name) + value = property(get_value, set_value) + + def __str__(self): + s = [x.__str__() for x in self.contents] + return '\n'.join(s) + + def finditer(self, key): + for x in self.contents[::-1]: + if hasattr(x, 'name') and x.name==key: + yield x + + def find(self, key): + for x in self.finditer(key): + return x + raise KeyError(key) + + +def _make_xform_property(myattrname, srcattrname=None): + private_attrname = myattrname + 'value' + private_srcname = myattrname + 'source' + if srcattrname is None: + srcattrname = myattrname + + def getfn(self): + srcobj = getattr(self, private_srcname) + if srcobj is not None: + return getattr(srcobj, srcattrname) + else: + return getattr(self, private_attrname) + + def setfn(self, value): + srcobj = getattr(self, private_srcname) + if srcobj is not None: + setattr(srcobj, srcattrname, value) + else: + setattr(self, private_attrname, value) + + return property(getfn, setfn) + + +class INISection(config.ConfigNamespace): + _lines = None + _options = None + _defaults = None + _optionxformvalue = None + _optionxformsource = None + _compat_skip_empty_lines = set() + def __init__(self, lineobj, defaults = None, + optionxformvalue=None, optionxformsource=None): + self._lines = [lineobj] + self._defaults = defaults + self._optionxformvalue = optionxformvalue + self._optionxformsource = optionxformsource + self._options = {} + + _optionxform = _make_xform_property('_optionxform') + + def _compat_get(self, key): + # identical to __getitem__ except that _compat_XXX + # is checked for backward-compatible handling + if key == '__name__': + return self._lines[-1].name + if self._optionxform: key = self._optionxform(key) + try: + value = self._options[key].value + del_empty = key in self._compat_skip_empty_lines + except KeyError: + if self._defaults and key in self._defaults._options: + value = self._defaults._options[key].value + del_empty = key in self._defaults._compat_skip_empty_lines + else: + raise + if del_empty: + value = re.sub('\n+', '\n', value) + return value + + def __getitem__(self, key): + if key == '__name__': + return self._lines[-1].name + if self._optionxform: key = self._optionxform(key) + try: + return self._options[key].value + except KeyError: + if self._defaults and key in self._defaults._options: + return self._defaults._options[key].value + else: + raise + + def __setitem__(self, key, value): + if self._optionxform: xkey = self._optionxform(key) + else: xkey = key + if xkey in self._compat_skip_empty_lines: + self._compat_skip_empty_lines.remove(xkey) + if xkey not in self._options: + # create a dummy object - value may have multiple lines + obj = LineContainer(OptionLine(key, '')) + self._lines[-1].add(obj) + self._options[xkey] = obj + # the set_value() function in LineContainer + # automatically handles multi-line values + self._options[xkey].value = value + + def __delitem__(self, key): + if self._optionxform: key = self._optionxform(key) + if key in self._compat_skip_empty_lines: + self._compat_skip_empty_lines.remove(key) + for l in self._lines: + remaining = [] + for o in l.contents: + if isinstance(o, LineContainer): + n = o.name + if self._optionxform: n = self._optionxform(n) + if key != n: remaining.append(o) + else: + remaining.append(o) + l.contents = remaining + del self._options[key] + + def __iter__(self): + d = set() + for l in self._lines: + for x in l.contents: + if isinstance(x, LineContainer): + if self._optionxform: + ans = self._optionxform(x.name) + else: + ans = x.name + if ans not in d: + yield ans + d.add(ans) + if self._defaults: + for x in self._defaults: + if x not in d: + yield x + d.add(x) + + def _new_namespace(self, name): + raise Exception('No sub-sections allowed', name) + + +def make_comment(line): + return CommentLine(line.rstrip('\n')) + + +def readline_iterator(f): + """iterate over a file by only using the file object's readline method""" + + have_newline = False + while True: + line = f.readline() + + if not line: + if have_newline: + yield "" + return + + if line.endswith('\n'): + have_newline = True + else: + have_newline = False + + yield line + + +def lower(x): + return x.lower() + + +class INIConfig(config.ConfigNamespace): + _data = None + _sections = None + _defaults = None + _optionxformvalue = None + _optionxformsource = None + _sectionxformvalue = None + _sectionxformsource = None + _parse_exc = None + _bom = False + def __init__(self, fp=None, defaults=None, parse_exc=True, + optionxformvalue=lower, optionxformsource=None, + sectionxformvalue=None, sectionxformsource=None): + self._data = LineContainer() + self._parse_exc = parse_exc + self._optionxformvalue = optionxformvalue + self._optionxformsource = optionxformsource + self._sectionxformvalue = sectionxformvalue + self._sectionxformsource = sectionxformsource + self._sections = {} + if defaults is None: defaults = {} + self._defaults = INISection(LineContainer(), optionxformsource=self) + for name, value in defaults.iteritems(): + self._defaults[name] = value + if fp is not None: + self._readfp(fp) + + _optionxform = _make_xform_property('_optionxform', 'optionxform') + _sectionxform = _make_xform_property('_sectionxform', 'optionxform') + + def __getitem__(self, key): + if key == DEFAULTSECT: + return self._defaults + if self._sectionxform: key = self._sectionxform(key) + return self._sections[key] + + def __setitem__(self, key, value): + raise Exception('Values must be inside sections', key, value) + + def __delitem__(self, key): + if self._sectionxform: key = self._sectionxform(key) + for line in self._sections[key]._lines: + self._data.contents.remove(line) + del self._sections[key] + + def __iter__(self): + d = set() + d.add(DEFAULTSECT) + for x in self._data.contents: + if isinstance(x, LineContainer): + if x.name not in d: + yield x.name + d.add(x.name) + + def _new_namespace(self, name): + if self._data.contents: + self._data.add(EmptyLine()) + obj = LineContainer(SectionLine(name)) + self._data.add(obj) + if self._sectionxform: name = self._sectionxform(name) + if name in self._sections: + ns = self._sections[name] + ns._lines.append(obj) + else: + ns = INISection(obj, defaults=self._defaults, + optionxformsource=self) + self._sections[name] = ns + return ns + + def __str__(self): + if self._bom: + fmt = u'\ufeff%s' + else: + fmt = '%s' + return fmt % self._data.__str__() + + __unicode__ = __str__ + + _line_types = [EmptyLine, CommentLine, + SectionLine, OptionLine, + ContinuationLine] + + def _parse(self, line): + for linetype in self._line_types: + lineobj = linetype.parse(line) + if lineobj: + return lineobj + else: + # can't parse line + return None + + def _readfp(self, fp): + cur_section = None + cur_option = None + cur_section_name = None + cur_option_name = None + pending_lines = [] + pending_empty_lines = False + try: + fname = fp.name + except AttributeError: + fname = '<???>' + linecount = 0 + exc = None + line = None + + for line in readline_iterator(fp): + # Check for BOM on first line + if linecount == 0 and isinstance(line, unicode): + if line[0] == u'\ufeff': + line = line[1:] + self._bom = True + + lineobj = self._parse(line) + linecount += 1 + + if not cur_section and not isinstance(lineobj, + (CommentLine, EmptyLine, SectionLine)): + if self._parse_exc: + raise MissingSectionHeaderError(fname, linecount, line) + else: + lineobj = make_comment(line) + + if lineobj is None: + if self._parse_exc: + if exc is None: exc = ParsingError(fname) + exc.append(linecount, line) + lineobj = make_comment(line) + + if isinstance(lineobj, ContinuationLine): + if cur_option: + if pending_lines: + cur_option.extend(pending_lines) + pending_lines = [] + if pending_empty_lines: + optobj._compat_skip_empty_lines.add(cur_option_name) + pending_empty_lines = False + cur_option.add(lineobj) + else: + # illegal continuation line - convert to comment + if self._parse_exc: + if exc is None: exc = ParsingError(fname) + exc.append(linecount, line) + lineobj = make_comment(line) + + if isinstance(lineobj, OptionLine): + if pending_lines: + cur_section.extend(pending_lines) + pending_lines = [] + pending_empty_lines = False + cur_option = LineContainer(lineobj) + cur_section.add(cur_option) + if self._optionxform: + cur_option_name = self._optionxform(cur_option.name) + else: + cur_option_name = cur_option.name + if cur_section_name == DEFAULTSECT: + optobj = self._defaults + else: + optobj = self._sections[cur_section_name] + optobj._options[cur_option_name] = cur_option + + if isinstance(lineobj, SectionLine): + self._data.extend(pending_lines) + pending_lines = [] + pending_empty_lines = False + cur_section = LineContainer(lineobj) + self._data.add(cur_section) + cur_option = None + cur_option_name = None + if cur_section.name == DEFAULTSECT: + self._defaults._lines.append(cur_section) + cur_section_name = DEFAULTSECT + else: + if self._sectionxform: + cur_section_name = self._sectionxform(cur_section.name) + else: + cur_section_name = cur_section.name + if cur_section_name not in self._sections: + self._sections[cur_section_name] = \ + INISection(cur_section, defaults=self._defaults, + optionxformsource=self) + else: + self._sections[cur_section_name]._lines.append(cur_section) + + if isinstance(lineobj, (CommentLine, EmptyLine)): + pending_lines.append(lineobj) + if isinstance(lineobj, EmptyLine): + pending_empty_lines = True + + self._data.extend(pending_lines) + if line and line[-1]=='\n': + self._data.add(EmptyLine()) + + if exc: + raise exc + diff --git a/python-iniparse.spec b/python-iniparse.spec new file mode 100644 index 0000000..8ebd41f --- /dev/null +++ b/python-iniparse.spec @@ -0,0 +1,72 @@ +%{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} + +Name: python-iniparse +Version: 0.3.1 +Release: 1%{?dist} +Summary: Python Module for Accessing and Modifying Configuration Data in INI files +Group: Development/Libraries +License: MIT +URL: http://code.google.com/p/iniparse/ +Source0: http://iniparse.googlecode.com/files/iniparse-%{version}.tar.gz +BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) + +BuildRequires: python-setuptools + +BuildArch: noarch + +%description +iniparse is an INI parser for Python which is API compatible +with the standard library's ConfigParser, preserves structure of INI +files (order of sections & options, indentation, comments, and blank +lines are preserved when data is updated), and is more convenient to +use. + +%prep +%setup -q -n iniparse-%{version} + + +%build +%{__python} setup.py build + +%install +rm -rf $RPM_BUILD_ROOT +%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT +# fixes +chmod 644 $RPM_BUILD_ROOT//usr/share/doc/iniparse-%{version}/index.html +mv $RPM_BUILD_ROOT/usr/share/doc/iniparse-%{version} $RPM_BUILD_ROOT/usr/share/doc/python-iniparse-%{version} + +%clean +rm -rf $RPM_BUILD_ROOT + + +%files +%defattr(-,root,root,-) +%doc %{_docdir}/python-iniparse-%{version}/* +%{python_sitelib}/iniparse +%{python_sitelib}/iniparse-%{version}-py*.egg-info + + + +%changelog +* Mon Mar 2 2009 Paramjit Oberoi <param@cs.wisc.edu> - 0.3.1-1 +- Release 0.3.1 +* Fri Feb 27 2009 Paramjit Oberoi <param@cs.wisc.edu> - 0.3.0-1 +- Release 0.3.0 +* Tue Dec 6 2008 Paramjit Oberoi <param@cs.wisc.edu> - 0.2.4-1 +- Release 0.2.4 +- added egg-info file to %%files +* Tue Dec 11 2007 Paramjit Oberoi <param@cs.wisc.edu> - 0.2.3-1 +- Release 0.2.3 +* Tue Sep 24 2007 Paramjit Oberoi <param@cs.wisc.edu> - 0.2.2-1 +- Release 0.2.2 +* Tue Aug 7 2007 Paramjit Oberoi <param@cs.wisc.edu> - 0.2.1-1 +- Release 0.2.1 +* Fri Jul 27 2007 Tim Lauridsen <timlau@fedoraproject.org> - 0.2-3 +- relocated doc to %{_docdir}/python-iniparse-%{version} +* Thu Jul 26 2007 Tim Lauridsen <timlau@fedoraproject.org> - 0.2-2 +- changed name from iniparse to python-iniparse +* Tue Jul 17 2007 Tim Lauridsen <timlau@fedoraproject.org> - 0.2-1 +- Release 0.2 +- Added html/* to %%doc +* Fri Jul 13 2007 Tim Lauridsen <timlau@fedoraproject.org> - 0.1-1 +- Initial build. diff --git a/runtests.py b/runtests.py new file mode 100755 index 0000000..18304dd --- /dev/null +++ b/runtests.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +import sys +import tests + +if __name__ == '__main__': + if len(sys.argv) == 2 and sys.argv[1] == '-g': + import unittestgui + unittestgui.main('tests.suite') + else: + import unittest + unittest.main(defaultTest='tests.suite') diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6d17ce3 --- /dev/null +++ b/setup.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +from distutils.core import setup + +VERSION = '0.3.1' + +setup(name ='iniparse', + version = VERSION, + description = 'Accessing and Modifying INI files', + author = 'Paramjit Oberoi', + author_email = 'param@cs.wisc.edu', + url = 'http://code.google.com/p/iniparse/', + license = 'MIT', + long_description = '''\ +iniparse is an INI parser for Python which is API compatible +with the standard library's ConfigParser, preserves structure of INI +files (order of sections & options, indentation, comments, and blank +lines are preserved when data is updated), and is more convenient to +use.''', + classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'License :: OSI Approved :: Python Software Foundation License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.4', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + packages = ['iniparse'], + data_files = [ + ('share/doc/iniparse-%s' % VERSION, ['README', 'LICENSE-PSF', + 'LICENSE', 'Changelog', + 'html/index.html', + 'html/style.css', + ]), + ], +) + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..beea5e3 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,21 @@ +import unittest, doctest + +import test_ini +import test_misc +import test_fuzz +import test_compat +import test_unicode +from iniparse import config +from iniparse import ini + +class suite(unittest.TestSuite): + def __init__(self): + unittest.TestSuite.__init__(self, [ + doctest.DocTestSuite(config), + doctest.DocTestSuite(ini), + test_ini.suite(), + test_misc.suite(), + test_fuzz.suite(), + test_compat.suite(), + test_unicode.suite(), + ]) diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 0000000..b8da3d5 --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,497 @@ +from iniparse import compat as ConfigParser +import StringIO +import unittest +import UserDict + +from test import test_support + +class SortedDict(UserDict.UserDict): + def items(self): + result = self.data.items() + result.sort() + return result + + def keys(self): + result = self.data.keys() + result.sort() + return result + + def values(self): + result = self.items() + return [i[1] for i in values] + + def iteritems(self): return iter(self.items()) + def iterkeys(self): return iter(self.keys()) + __iter__ = iterkeys + def itervalues(self): return iter(self.values()) + +class TestCaseBase(unittest.TestCase): + def newconfig(self, defaults=None): + if defaults is None: + self.cf = self.config_class() + else: + self.cf = self.config_class(defaults) + return self.cf + + def fromstring(self, string, defaults=None): + cf = self.newconfig(defaults) + sio = StringIO.StringIO(string) + cf.readfp(sio) + return cf + + def test_basic(self): + cf = self.fromstring( + "[Foo Bar]\n" + "foo=bar\n" + "[Spacey Bar]\n" + "foo = bar\n" + "[Commented Bar]\n" + "foo: bar ; comment\n" + "[Long Line]\n" + "foo: this line is much, much longer than my editor\n" + " likes it.\n" + "[Section\\with$weird%characters[\t]\n" + "[Internationalized Stuff]\n" + "foo[bg]: Bulgarian\n" + "foo=Default\n" + "foo[en]=English\n" + "foo[de]=Deutsch\n" + "[Spaces]\n" + "key with spaces : value\n" + "another with spaces = splat!\n" + ) + L = cf.sections() + L.sort() + eq = self.assertEqual + eq(L, [r'Commented Bar', + r'Foo Bar', + r'Internationalized Stuff', + r'Long Line', + r'Section\with$weird%characters[' '\t', + r'Spaces', + r'Spacey Bar', + ]) + + # The use of spaces in the section names serves as a + # regression test for SourceForge bug #583248: + # http://www.python.org/sf/583248 + eq(cf.get('Foo Bar', 'foo'), 'bar') + eq(cf.get('Spacey Bar', 'foo'), 'bar') + eq(cf.get('Commented Bar', 'foo'), 'bar') + eq(cf.get('Spaces', 'key with spaces'), 'value') + eq(cf.get('Spaces', 'another with spaces'), 'splat!') + + self.failIf('__name__' in cf.options("Foo Bar"), + '__name__ "option" should not be exposed by the API!') + + # Make sure the right things happen for remove_option(); + # added to include check for SourceForge bug #123324: + self.failUnless(cf.remove_option('Foo Bar', 'foo'), + "remove_option() failed to report existance of option") + self.failIf(cf.has_option('Foo Bar', 'foo'), + "remove_option() failed to remove option") + self.failIf(cf.remove_option('Foo Bar', 'foo'), + "remove_option() failed to report non-existance of option" + " that was removed") + + self.assertRaises(ConfigParser.NoSectionError, + cf.remove_option, 'No Such Section', 'foo') + + eq(cf.get('Long Line', 'foo'), + 'this line is much, much longer than my editor\nlikes it.') + + def test_case_sensitivity(self): + cf = self.newconfig() + cf.add_section("A") + cf.add_section("a") + L = cf.sections() + L.sort() + eq = self.assertEqual + eq(L, ["A", "a"]) + cf.set("a", "B", "value") + eq(cf.options("a"), ["b"]) + eq(cf.get("a", "b"), "value", + "could not locate option, expecting case-insensitive option names") + self.failUnless(cf.has_option("a", "b")) + cf.set("A", "A-B", "A-B value") + for opt in ("a-b", "A-b", "a-B", "A-B"): + self.failUnless( + cf.has_option("A", opt), + "has_option() returned false for option which should exist") + eq(cf.options("A"), ["a-b"]) + eq(cf.options("a"), ["b"]) + cf.remove_option("a", "B") + eq(cf.options("a"), []) + + # SF bug #432369: + cf = self.fromstring( + "[MySection]\nOption: first line\n\tsecond line\n") + eq(cf.options("MySection"), ["option"]) + eq(cf.get("MySection", "Option"), "first line\nsecond line") + + # SF bug #561822: + cf = self.fromstring("[section]\nnekey=nevalue\n", + defaults={"key":"value"}) + self.failUnless(cf.has_option("section", "Key")) + + + def test_default_case_sensitivity(self): + cf = self.newconfig({"foo": "Bar"}) + self.assertEqual( + cf.get("DEFAULT", "Foo"), "Bar", + "could not locate option, expecting case-insensitive option names") + cf = self.newconfig({"Foo": "Bar"}) + self.assertEqual( + cf.get("DEFAULT", "Foo"), "Bar", + "could not locate option, expecting case-insensitive defaults") + + def test_parse_errors(self): + self.newconfig() + self.parse_error(ConfigParser.ParsingError, + "[Foo]\n extra-spaces: splat\n") + self.parse_error(ConfigParser.ParsingError, + "[Foo]\n extra-spaces= splat\n") + self.parse_error(ConfigParser.ParsingError, + "[Foo]\noption-without-value\n") + self.parse_error(ConfigParser.ParsingError, + "[Foo]\n:value-without-option-name\n") + self.parse_error(ConfigParser.ParsingError, + "[Foo]\n=value-without-option-name\n") + self.parse_error(ConfigParser.MissingSectionHeaderError, + "No Section!\n") + + def parse_error(self, exc, src): + sio = StringIO.StringIO(src) + self.assertRaises(exc, self.cf.readfp, sio) + + def test_query_errors(self): + cf = self.newconfig() + self.assertEqual(cf.sections(), [], + "new ConfigParser should have no defined sections") + self.failIf(cf.has_section("Foo"), + "new ConfigParser should have no acknowledged sections") + self.assertRaises(ConfigParser.NoSectionError, + cf.options, "Foo") + self.assertRaises(ConfigParser.NoSectionError, + cf.set, "foo", "bar", "value") + self.get_error(ConfigParser.NoSectionError, "foo", "bar") + cf.add_section("foo") + self.get_error(ConfigParser.NoOptionError, "foo", "bar") + + def get_error(self, exc, section, option): + try: + self.cf.get(section, option) + except exc, e: + return e + else: + self.fail("expected exception type %s.%s" + % (exc.__module__, exc.__name__)) + + def test_boolean(self): + cf = self.fromstring( + "[BOOLTEST]\n" + "T1=1\n" + "T2=TRUE\n" + "T3=True\n" + "T4=oN\n" + "T5=yes\n" + "F1=0\n" + "F2=FALSE\n" + "F3=False\n" + "F4=oFF\n" + "F5=nO\n" + "E1=2\n" + "E2=foo\n" + "E3=-1\n" + "E4=0.1\n" + "E5=FALSE AND MORE" + ) + for x in range(1, 5): + self.failUnless(cf.getboolean('BOOLTEST', 't%d' % x)) + self.failIf(cf.getboolean('BOOLTEST', 'f%d' % x)) + self.assertRaises(ValueError, + cf.getboolean, 'BOOLTEST', 'e%d' % x) + + def test_weird_errors(self): + cf = self.newconfig() + cf.add_section("Foo") + self.assertRaises(ConfigParser.DuplicateSectionError, + cf.add_section, "Foo") + + def test_write(self): + cf = self.fromstring( + "[Long Line]\n" + "foo: this line is much, much longer than my editor\n" + " likes it.\n" + "[DEFAULT]\n" + "foo: another very\n" + " long line" + ) + output = StringIO.StringIO() + cf.write(output) + self.assertEqual( + output.getvalue(), + "[Long Line]\n" + "foo: this line is much, much longer than my editor\n" + " likes it.\n" + "[DEFAULT]\n" + "foo: another very\n" + " long line" + ) + + def test_set_string_types(self): + cf = self.fromstring("[sect]\n" + "option1=foo\n") + # Check that we don't get an exception when setting values in + # an existing section using strings: + class mystr(str): + pass + cf.set("sect", "option1", "splat") + cf.set("sect", "option1", mystr("splat")) + cf.set("sect", "option2", "splat") + cf.set("sect", "option2", mystr("splat")) + try: + unicode + except NameError: + pass + else: + cf.set("sect", "option1", unicode("splat")) + cf.set("sect", "option2", unicode("splat")) + + def test_read_returns_file_list(self): + file1 = test_support.findfile("cfgparser.1") + # check when we pass a mix of readable and non-readable files: + cf = self.newconfig() + parsed_files = cf.read([file1, "nonexistant-file"]) + self.assertEqual(parsed_files, [file1]) + self.assertEqual(cf.get("Foo Bar", "foo"), "newbar") + # check when we pass only a filename: + cf = self.newconfig() + parsed_files = cf.read(file1) + self.assertEqual(parsed_files, [file1]) + self.assertEqual(cf.get("Foo Bar", "foo"), "newbar") + # check when we pass only missing files: + cf = self.newconfig() + parsed_files = cf.read(["nonexistant-file"]) + self.assertEqual(parsed_files, []) + # check when we pass no files: + cf = self.newconfig() + parsed_files = cf.read([]) + self.assertEqual(parsed_files, []) + + # shared by subclasses + def get_interpolation_config(self): + return self.fromstring( + "[Foo]\n" + "bar=something %(with1)s interpolation (1 step)\n" + "bar9=something %(with9)s lots of interpolation (9 steps)\n" + "bar10=something %(with10)s lots of interpolation (10 steps)\n" + "bar11=something %(with11)s lots of interpolation (11 steps)\n" + "with11=%(with10)s\n" + "with10=%(with9)s\n" + "with9=%(with8)s\n" + "with8=%(With7)s\n" + "with7=%(WITH6)s\n" + "with6=%(with5)s\n" + "With5=%(with4)s\n" + "WITH4=%(with3)s\n" + "with3=%(with2)s\n" + "with2=%(with1)s\n" + "with1=with\n" + "\n" + "[Mutual Recursion]\n" + "foo=%(bar)s\n" + "bar=%(foo)s\n" + "\n" + "[Interpolation Error]\n" + "name=%(reference)s\n", + # no definition for 'reference' + defaults={"getname": "%(__name__)s"}) + + def check_items_config(self, expected): + cf = self.fromstring( + "[section]\n" + "name = value\n" + "key: |%(name)s| \n" + "getdefault: |%(default)s|\n" + "getname: |%(__name__)s|", + defaults={"default": "<default>"}) + L = list(cf.items("section")) + L.sort() + self.assertEqual(L, expected) + + +class ConfigParserTestCase(TestCaseBase): + config_class = ConfigParser.ConfigParser + + def test_interpolation(self): + cf = self.get_interpolation_config() + eq = self.assertEqual + eq(cf.get("Foo", "getname"), "Foo") + eq(cf.get("Foo", "bar"), "something with interpolation (1 step)") + eq(cf.get("Foo", "bar9"), + "something with lots of interpolation (9 steps)") + eq(cf.get("Foo", "bar10"), + "something with lots of interpolation (10 steps)") + self.get_error(ConfigParser.InterpolationDepthError, "Foo", "bar11") + + def test_interpolation_missing_value(self): + cf = self.get_interpolation_config() + e = self.get_error(ConfigParser.InterpolationError, + "Interpolation Error", "name") + self.assertEqual(e.reference, "reference") + self.assertEqual(e.section, "Interpolation Error") + self.assertEqual(e.option, "name") + + def test_items(self): + self.check_items_config([('default', '<default>'), + ('getdefault', '|<default>|'), + ('getname', '|section|'), + ('key', '|value|'), + ('name', 'value')]) + + def test_set_nonstring_types(self): + cf = self.newconfig() + cf.add_section('non-string') + cf.set('non-string', 'int', 1) + cf.set('non-string', 'list', [0, 1, 1, 2, 3, 5, 8, 13, '%(']) + cf.set('non-string', 'dict', {'pi': 3.14159, '%(': 1, + '%(list)': '%(list)'}) + cf.set('non-string', 'string_with_interpolation', '%(list)s') + self.assertEqual(cf.get('non-string', 'int', raw=True), 1) + self.assertRaises(TypeError, cf.get, 'non-string', 'int') + self.assertEqual(cf.get('non-string', 'list', raw=True), + [0, 1, 1, 2, 3, 5, 8, 13, '%(']) + self.assertRaises(TypeError, cf.get, 'non-string', 'list') + self.assertEqual(cf.get('non-string', 'dict', raw=True), + {'pi': 3.14159, '%(': 1, '%(list)': '%(list)'}) + self.assertRaises(TypeError, cf.get, 'non-string', 'dict') + self.assertEqual(cf.get('non-string', 'string_with_interpolation', + raw=True), '%(list)s') + self.assertRaises(ValueError, cf.get, 'non-string', + 'string_with_interpolation', raw=False) + + +class RawConfigParserTestCase(TestCaseBase): + config_class = ConfigParser.RawConfigParser + + def test_interpolation(self): + cf = self.get_interpolation_config() + eq = self.assertEqual + eq(cf.get("Foo", "getname"), "%(__name__)s") + eq(cf.get("Foo", "bar"), + "something %(with1)s interpolation (1 step)") + eq(cf.get("Foo", "bar9"), + "something %(with9)s lots of interpolation (9 steps)") + eq(cf.get("Foo", "bar10"), + "something %(with10)s lots of interpolation (10 steps)") + eq(cf.get("Foo", "bar11"), + "something %(with11)s lots of interpolation (11 steps)") + + def test_items(self): + self.check_items_config([('default', '<default>'), + ('getdefault', '|%(default)s|'), + ('getname', '|%(__name__)s|'), + ('key', '|%(name)s|'), + ('name', 'value')]) + + def test_set_nonstring_types(self): + cf = self.newconfig() + cf.add_section('non-string') + cf.set('non-string', 'int', 1) + cf.set('non-string', 'list', [0, 1, 1, 2, 3, 5, 8, 13]) + cf.set('non-string', 'dict', {'pi': 3.14159}) + self.assertEqual(cf.get('non-string', 'int'), 1) + self.assertEqual(cf.get('non-string', 'list'), + [0, 1, 1, 2, 3, 5, 8, 13]) + self.assertEqual(cf.get('non-string', 'dict'), {'pi': 3.14159}) + + +class SafeConfigParserTestCase(ConfigParserTestCase): + config_class = ConfigParser.SafeConfigParser + + def test_safe_interpolation(self): + # See http://www.python.org/sf/511737 + cf = self.fromstring("[section]\n" + "option1=xxx\n" + "option2=%(option1)s/xxx\n" + "ok=%(option1)s/%%s\n" + "not_ok=%(option2)s/%%s") + self.assertEqual(cf.get("section", "ok"), "xxx/%s") + self.assertEqual(cf.get("section", "not_ok"), "xxx/xxx/%s") + + def test_set_malformatted_interpolation(self): + cf = self.fromstring("[sect]\n" + "option1=foo\n") + + self.assertEqual(cf.get('sect', "option1"), "foo") + + self.assertRaises(ValueError, cf.set, "sect", "option1", "%foo") + self.assertRaises(ValueError, cf.set, "sect", "option1", "foo%") + self.assertRaises(ValueError, cf.set, "sect", "option1", "f%oo") + + self.assertEqual(cf.get('sect', "option1"), "foo") + + def test_set_nonstring_types(self): + cf = self.fromstring("[sect]\n" + "option1=foo\n") + # Check that we get a TypeError when setting non-string values + # in an existing section: + self.assertRaises(TypeError, cf.set, "sect", "option1", 1) + self.assertRaises(TypeError, cf.set, "sect", "option1", 1.0) + self.assertRaises(TypeError, cf.set, "sect", "option1", object()) + self.assertRaises(TypeError, cf.set, "sect", "option2", 1) + self.assertRaises(TypeError, cf.set, "sect", "option2", 1.0) + self.assertRaises(TypeError, cf.set, "sect", "option2", object()) + + def test_add_section_default_1(self): + cf = self.newconfig() + self.assertRaises(ValueError, cf.add_section, "default") + + def test_add_section_default_2(self): + cf = self.newconfig() + self.assertRaises(ValueError, cf.add_section, "DEFAULT") + +class SortedTestCase(RawConfigParserTestCase): + def newconfig(self, defaults=None): + self.cf = self.config_class(defaults=defaults, dict_type=SortedDict) + return self.cf + + def test_sorted(self): + self.fromstring("[b]\n" + "o4=1\n" + "o3=2\n" + "o2=3\n" + "o1=4\n" + "[a]\n" + "k=v\n") + output = StringIO.StringIO() + self.cf.write(output) + self.assertEquals(output.getvalue(), + "[a]\n" + "k = v\n\n" + "[b]\n" + "o1 = 4\n" + "o2 = 3\n" + "o3 = 2\n" + "o4 = 1\n\n") + +def test_main(): + test_support.run_unittest( + ConfigParserTestCase, + RawConfigParserTestCase, + SafeConfigParserTestCase, + SortedTestCase + ) + +class suite(unittest.TestSuite): + def __init__(self): + unittest.TestSuite.__init__(self, [ + unittest.makeSuite(RawConfigParserTestCase, 'test'), + unittest.makeSuite(ConfigParserTestCase, 'test'), + unittest.makeSuite(SafeConfigParserTestCase, 'test'), + #unittest.makeSuite(SortedTestCase, 'test'), + ]) + +if __name__ == "__main__": + test_main() diff --git a/tests/test_fuzz.py b/tests/test_fuzz.py new file mode 100644 index 0000000..773e1fc --- /dev/null +++ b/tests/test_fuzz.py @@ -0,0 +1,111 @@ +import re +import random +import unittest +import ConfigParser +from textwrap import dedent +from StringIO import StringIO +from iniparse import compat, ini + +# TODO: +# tabs +# substitutions + +def random_string(maxlen=200): + length = random.randint(0, maxlen) + s = [] + for i in range(length): + s.append(chr(random.randint(32, 126))) + + return ''.join(s) + +def random_space(maxlen=10): + length = random.randint(0, maxlen) + return ' '*length + +def random_ini_file(): + num_lines = random.randint(0, 100) + lines = [] + for i in range(num_lines): + x = random.random() + if x < 0.1: + # empty line + lines.append(random_space()) + elif x < 0.3: + # comment + sep = random.choice(['#', ';']) + lines.append(sep + random_string()) + elif x < 0.5: + # section + if random.random() < 0.1: + name = 'DEFAULT' + else: + name = random_string() + name = re.sub(']', '' , name) + l = '[' + name + ']' + if random.randint(0,1): + l += random_space() + if random.randint(0,1): + sep = random.choice(['#', ';']) + l += sep + random_string() + lines.append(l) + elif x < 0.7: + # option + name = random_string() + name = re.sub(':|=| |\[', '', name) + sep = random.choice([':', '=']) + l = name + random_space() + sep + random_space() + random_string() + if random.randint(0,1): + l += ' ' + random_space() + ';' +random_string() + lines.append(l) + elif x < 0.9: + # continuation + lines.append(' ' + random_space() + random_string()) + else: + # junk + lines.append(random_string()) + + return '\n'.join(lines) + +class test_fuzz(unittest.TestCase): + def test_fuzz(self): + random.seed(42) + for i in range(100): + # parse random file with errors disabled + s = random_ini_file() + c = ini.INIConfig(parse_exc=False) + c._readfp(StringIO(s)) + # check that file is preserved, except for + # commenting out erroneous lines + l1 = s.split('\n') + l2 = str(c).split('\n') + self.assertEqual(len(l1), len(l2)) + good_lines = [] + for i in range(len(l1)): + try: + self.assertEqual(l1[i], l2[i]) + good_lines.append(l1[i]) + except AssertionError: + self.assertEqual('#'+l1[i], l2[i]) + # parse the good subset of the file + # using ConfigParser + s = '\n'.join(good_lines) + cc = compat.RawConfigParser() + cc.readfp(StringIO(s)) + cc_py = ConfigParser.RawConfigParser() + cc_py.readfp(StringIO(s)) + # compare the two configparsers + self.assertEqualSorted(cc_py.sections(), cc.sections()) + self.assertEqualSorted(cc_py.defaults().items(), cc.defaults().items()) + for sec in cc_py.sections(): + self.assertEqualSorted(cc_py.items(sec), cc.items(sec)) + + def assertEqualSorted(self, l1, l2): + l1.sort() + l2.sort() + self.assertEqual(l1, l2) + +class suite(unittest.TestSuite): + def __init__(self): + unittest.TestSuite.__init__(self, [ + unittest.makeSuite(test_fuzz, 'test'), + ]) diff --git a/tests/test_ini.py b/tests/test_ini.py new file mode 100644 index 0000000..fdf9e5b --- /dev/null +++ b/tests/test_ini.py @@ -0,0 +1,391 @@ +import unittest +from StringIO import StringIO + +from iniparse import ini +from iniparse import compat +from iniparse import config + +class test_section_line(unittest.TestCase): + invalid_lines = [ + '# this is a comment', + '; this is a comment', + ' [sections must start on column1]', + '[incomplete', + '[ no closing ]brackets]', + 'ice-cream = mmmm', + 'ic[e-c]ream = mmmm', + '[ice-cream] = mmmm', + '-$%^', + ] + def test_invalid(self): + for l in self.invalid_lines: + p = ini.SectionLine.parse(l) + self.assertEqual(p, None) + + lines = [ + ('[section]' , ('section', None, None, -1)), + ('[se\ct%[ion\t]' , ('se\ct%[ion\t', None, None, -1)), + ('[sec tion] ; hi' , ('sec tion', ' hi', ';', 12)), + ('[section] #oops!' , ('section', 'oops!', '#', 11)), + ('[section] ; ' , ('section', '', ';', 12)), + ('[section] ' , ('section', None, None, -1)), + ] + def test_parsing(self): + for l in self.lines: + p = ini.SectionLine.parse(l[0]) + self.assertNotEqual(p, None) + self.assertEqual(p.name, l[1][0]) + self.assertEqual(p.comment, l[1][1]) + self.assertEqual(p.comment_separator, l[1][2]) + self.assertEqual(p.comment_offset, l[1][3]) + + def test_printing(self): + for l in self.lines: + p = ini.SectionLine.parse(l[0]) + self.assertEqual(str(p), l[0]) + self.assertEqual(p.to_string(), l[0].strip()) + + indent_test_lines = [ + ('[oldname] ; comment', 'long new name', + '[long new name] ; comment'), + ('[oldname] ; comment', 'short', + '[short] ; comment'), + ('[oldname] ; comment', 'really long new name', + '[really long new name] ; comment'), + ] + def test_preserve_indentation(self): + for l in self.indent_test_lines: + p = ini.SectionLine.parse(l[0]) + p.name = l[1] + self.assertEqual(str(p), l[2]) + +class test_option_line(unittest.TestCase): + lines = [ + ('option = value', 'option', ' = ', 'value', None, None, -1), + ('option: value', 'option', ': ', 'value', None, None, -1), + ('option=value', 'option', '=', 'value', None, None, -1), + ('op[ti]on=value', 'op[ti]on', '=', 'value', None, None, -1), + + ('option = value # no comment', 'option', ' = ', 'value # no comment', + None, None, -1), + ('option = value ;', 'option', ' = ', 'value', + ';', '', 19), + ('option = value ; comment', 'option', ' = ', 'value', + ';', ' comment', 19), + ('option = value;1 ; comment', 'option', ' = ', 'value;1 ; comment', + None, None, -1), + ('op;ti on = value ;; comm;ent', 'op;ti on', ' = ', 'value', + ';', '; comm;ent', 22), + ] + def test_parsing(self): + for l in self.lines: + p = ini.OptionLine.parse(l[0]) + self.assertEqual(p.name, l[1]) + self.assertEqual(p.separator, l[2]) + self.assertEqual(p.value, l[3]) + self.assertEqual(p.comment_separator, l[4]) + self.assertEqual(p.comment, l[5]) + self.assertEqual(p.comment_offset, l[6]) + + invalid_lines = [ + ' option = value', + '# comment', + '; comment', + '[section 7]', + '[section=option]', + 'option', + ] + def test_invalid(self): + for l in self.invalid_lines: + p = ini.OptionLine.parse(l) + self.assertEqual(p, None) + + print_lines = [ + 'option = value', + 'option= value', + 'option : value', + 'option: value ', + 'option = value ', + 'option = value ;', + 'option = value;2 ;; 4 5', + 'option = value ; hi!', + ] + def test_printing(self): + for l in self.print_lines: + p = ini.OptionLine.parse(l) + self.assertEqual(str(p), l) + self.assertEqual(p.to_string(), l.rstrip()) + + indent_test_lines = [ + ('option = value ;comment', 'newoption', 'newval', + 'newoption = newval ;comment'), + ('option = value ;comment', 'newoption', 'newval', + 'newoption = newval ;comment'), + ] + def test_preserve_indentation(self): + for l in self.indent_test_lines: + p = ini.OptionLine.parse(l[0]) + p.name = l[1] + p.value = l[2] + self.assertEqual(str(p), l[3]) + +class test_comment_line(unittest.TestCase): + invalid_lines = [ + '[section]', + 'option = value ;comment', + ' # must start on first column', + ] + def test_invalid(self): + for l in self.invalid_lines: + p = ini.CommentLine.parse(l) + self.assertEqual(p, None) + + lines = [ + '#this is a comment', + ';; this is also a comment', + '; so is this ', + 'Rem and this', + 'remthis too!' + ] + def test_parsing(self): + for l in self.lines: + p = ini.CommentLine.parse(l) + self.assertEqual(str(p), l) + self.assertEqual(p.to_string(), l.rstrip()) + +class test_other_lines(unittest.TestCase): + def test_empty(self): + for s in ['asdf', '; hi', ' #rr', '[sec]', 'opt=val']: + self.assertEqual(ini.EmptyLine.parse(s), None) + for s in ['', ' ', '\t \t']: + self.assertEqual(str(ini.EmptyLine.parse(s)), s) + + def test_continuation(self): + for s in ['asdf', '; hi', '[sec]', 'a=3']: + self.assertEqual(ini.ContinuationLine.parse(s), None) + for s in [' asdfasd ', '\t mmmm']: + self.assertEqual(ini.ContinuationLine.parse(s).value, + s.strip()) + self.assertEqual(ini.ContinuationLine.parse(s).to_string(), + s.rstrip().replace('\t',' ')) + + +class test_ini(unittest.TestCase): + s1 = """ +[section1] +help = me +I'm = desperate ; really! + +[section2] +# comment and empty line before the first option + +just = what? +just = kidding + +[section1] +help = yourself +but = also me +""" + + def test_basic(self): + sio = StringIO(self.s1) + p = ini.INIConfig(sio) + self.assertEqual(str(p), self.s1) + self.assertEqual(p._data.find('section1').find('but').value, 'also me') + self.assertEqual(p._data.find('section1').find('help').value, 'yourself') + self.assertEqual(p._data.find('section2').find('just').value, 'kidding') + + itr = p._data.finditer('section1') + v = itr.next() + self.assertEqual(v.find('help').value, 'yourself') + self.assertEqual(v.find('but').value, 'also me') + v = itr.next() + self.assertEqual(v.find('help').value, 'me') + self.assertEqual(v.find('I\'m').value, 'desperate') + self.assertRaises(StopIteration, itr.next) + + self.assertRaises(KeyError, p._data.find, 'section') + self.assertRaises(KeyError, p._data.find('section2').find, 'ahem') + + def test_lookup(self): + sio = StringIO(self.s1) + p = ini.INIConfig(sio) + self.assertEqual(p.section1.help, 'yourself') + self.assertEqual(p.section1.but, 'also me') + self.assertEqual(getattr(p.section1, 'I\'m'), 'desperate') + self.assertEqual(p.section2.just, 'kidding') + + self.assertEqual(p.section1.just.__class__, config.Undefined) + self.assertEqual(p.section2.help.__class__, config.Undefined) + + def test_order(self): + sio = StringIO(self.s1) + p = ini.INIConfig(sio) + self.assertEqual(list(p), ['section1','section2']) + self.assertEqual(list(p.section1), ['help', "i'm", 'but']) + self.assertEqual(list(p.section2), ['just']) + + def test_delete(self): + sio = StringIO(self.s1) + p = ini.INIConfig(sio) + del p.section1.help + self.assertEqual(list(p.section1), ["i'm", 'but']) + self.assertEqual(str(p), """ +[section1] +I'm = desperate ; really! + +[section2] +# comment and empty line before the first option + +just = what? +just = kidding + +[section1] +but = also me +""") + del p.section2 + self.assertEqual(str(p), """ +[section1] +I'm = desperate ; really! + + +[section1] +but = also me +""") + + def check_order(self, c): + sio = StringIO(self.s1) + c = c({'pi':'3.14153'}) + c.readfp(sio) + self.assertEqual(c.sections(), ['section1','section2']) + self.assertEqual(c.options('section1'), ['help', "i'm", 'but', 'pi']) + self.assertEqual(c.items('section1'), [ + ('help', 'yourself'), + ("i'm", 'desperate'), + ('but', 'also me'), + ('pi', '3.14153'), + ]) + + def test_compat_order(self): + self.check_order(compat.RawConfigParser) + self.check_order(compat.ConfigParser) + + inv = ( +(""" +# values must be in a section +value = 5 +""", +""" +# values must be in a section +#value = 5 +"""), +(""" +# continuation lines only allowed after options +[section] +op1 = qwert + yuiop +op2 = qwert + + yuiop +op3 = qwert +# yup + yuiop + +[another section] + hmmm +""", +""" +# continuation lines only allowed after options +[section] +op1 = qwert + yuiop +op2 = qwert + + yuiop +op3 = qwert +# yup + yuiop + +[another section] +# hmmm +""")) + + def test_invalid(self): + for (org, mod) in self.inv: + ip = ini.INIConfig(StringIO(org), parse_exc=False) + self.assertEqual(str(ip), mod) + + # test multi-line values + s2 = ( +""" +[section] +option = + foo + bar + + baz + yam +""" +) + + s3 = ( +""" +[section] +option = + foo + bar + mum + + baz + yam +""" +) + + def test_option_continuation(self): + ip = ini.INIConfig(StringIO(self.s2)) + self.assertEqual(str(ip), self.s2) + value = ip.section.option.split('\n') + value.insert(3, 'mum') + ip.section.option = '\n'.join(value) + self.assertEqual(str(ip), self.s3) + + s5 = ( +""" +[section] +option = + foo + bar +""" +) + + s6 = ( +""" +[section] +option = + + + foo + + + +another = baz +""" +) + + def test_option_continuation_single(self): + ip = ini.INIConfig(StringIO(self.s5)) + self.assertEqual(str(ip), self.s5) + ip.section.option = '\n'.join(['', '', '', 'foo', '', '', '']) + ip.section.another = 'baz' + self.assertEqual(str(ip), self.s6) + + +class suite(unittest.TestSuite): + def __init__(self): + unittest.TestSuite.__init__(self, [ + unittest.makeSuite(test_section_line, 'test'), + unittest.makeSuite(test_option_line, 'test'), + unittest.makeSuite(test_comment_line, 'test'), + unittest.makeSuite(test_other_lines, 'test'), + unittest.makeSuite(test_ini, 'test'), + ]) diff --git a/tests/test_misc.py b/tests/test_misc.py new file mode 100644 index 0000000..aa83daf --- /dev/null +++ b/tests/test_misc.py @@ -0,0 +1,411 @@ +import unittest +import pickle +import ConfigParser +from textwrap import dedent +from StringIO import StringIO +from iniparse import compat, ini + +class CaseSensitiveConfigParser(compat.ConfigParser): + """Case Sensitive version of ConfigParser""" + def optionxform(self, option): + """Use str()""" + return str(option) + +class test_optionxform_override(unittest.TestCase): + def test_derivedclass(self): + c = CaseSensitiveConfigParser() + c.add_section('foo') + c.set('foo', 'bar', 'a') + c.set('foo', 'Bar', 'b') + self.assertEqual(c.get('foo', 'bar'), 'a') + self.assertEqual(c.get('foo', 'Bar'), 'b') + + def test_assignment(self): + c = compat.ConfigParser() + c.optionxform = str + c.add_section('foo') + c.set('foo', 'bar', 'a') + c.set('foo', 'Bar', 'b') + self.assertEqual(c.get('foo', 'bar'), 'a') + self.assertEqual(c.get('foo', 'Bar'), 'b') + + def test_dyanamic(self): + c = compat.ConfigParser() + c.optionxform = str + c.add_section('foo') + c.set('foo', 'bar', 'a') + c.set('foo', 'Bar', 'b') + c.set('foo', 'BAR', 'c') + c.optionxform = str.upper + self.assertEqual(c.get('foo', 'Bar'), 'c') + c.optionxform = str.lower + self.assertEqual(c.get('foo', 'Bar'), 'a') + c.optionxform = str + self.assertEqual(c.get('foo', 'Bar'), 'b') + + +class OnlyReadline: + def __init__(self, s): + self.sio = StringIO(s) + + def readline(self): + return self.sio.readline() + +class test_readline(unittest.TestCase): + """Test that the file object passed to readfp only needs to + support the .readline() method. As of Python-2.4.4, this is + true of the standard librariy's ConfigParser, and so other + code uses that to guide what is sufficiently file-like.""" + + test_strings = [ +"""\ +[foo] +bar=7 +baz=8""", +"""\ +[foo] +bar=7 +baz=8 +""", +"""\ +[foo] +bar=7 +baz=8 + """] + + def test_readline_iniconfig(self): + for s in self.test_strings: + fp = OnlyReadline(s) + c = ini.INIConfig() + c._readfp(fp) + self.assertEqual(s, str(c)) + + def test_readline_configparser(self): + for s in self.test_strings: + fp = OnlyReadline(s) + c = compat.ConfigParser() + c.readfp(fp) + ss = StringIO() + c.write(ss) + self.assertEqual(s, ss.getvalue()) + + +class test_multiline_with_comments(unittest.TestCase): + """Test that multiline values are allowed to span comments.""" + + s = """\ +[sec] +opt = 1 + 2 + +# comment + 3""" + + def test_read(self): + c = ini.INIConfig() + c._readfp(StringIO(self.s)) + self.assertEqual(c.sec.opt, '1\n2\n\n3') + + def test_write(self): + c = ini.INIConfig() + c._readfp(StringIO(self.s)) + c.sec.opt = 'xyz' + self.assertEqual(str(c), """\ +[sec] +opt = xyz""") + +class test_empty_file(unittest.TestCase): + """Test if it works with an blank file""" + + s = "" + + def test_read(self): + c = ini.INIConfig() + c._readfp(StringIO(self.s)) + self.assertEqual(str(c), '') + + def test_write(self): + c = ini.INIConfig() + c._readfp(StringIO(self.s)) + c.sec.opt = 'xyz' + self.assertEqual(str(c), """\ +[sec] +opt = xyz""") + +class test_custom_dict(unittest.TestCase): + def test_custom_dict_not_supported(self): + self.assertRaises(ValueError, compat.RawConfigParser, None, 'foo') + +class test_compat(unittest.TestCase): + """Miscellaneous compatibility tests.""" + + s = dedent("""\ + [DEFAULT] + pi = 3.1415 + three = 3 + poet = e e + + cummings + NH = + live free + + or die + + [sec] + opt = 6 + three = 3.0 + no-three = one + two + + four + longopt = foo + bar + + # empty line should not be part of value + baz + + bat + + """) + + def do_test(self, c): + # default section is not acknowledged + self.assertEqual(c.sections(), ['sec']) + # options in the default section are merged with other sections + self.assertEqual(sorted(c.options('sec')), + ['longopt', 'nh', 'no-three', 'opt', 'pi', 'poet', 'three']) + + # empty lines are stripped from multi-line values + self.assertEqual(c.get('sec', 'poet').split('\n'), + ['e e', 'cummings']) + self.assertEqual(c.get('DEFAULT', 'poet').split('\n'), + ['e e', 'cummings']) + self.assertEqual(c.get('sec', 'longopt').split('\n'), + ['foo', 'bar', 'baz', 'bat']) + self.assertEqual(c.get('sec', 'NH').split('\n'), + ['', 'live free', 'or die']) + + # check that empy-line stripping happens on all access paths + # defaults() + self.assertEqual(c.defaults(), { + 'poet': 'e e\ncummings', + 'nh': '\nlive free\nor die', + 'pi': '3.1415', + 'three': '3', + }) + # items() + l = c.items('sec') + l.sort() + self.assertEqual(l, [ + ('longopt', 'foo\nbar\nbaz\nbat'), + ('nh', '\nlive free\nor die'), + ('no-three', 'one\ntwo\nfour'), + ('opt', '6'), + ('pi', '3.1415'), + ('poet', 'e e\ncummings'), + ('three', '3.0'), + ]) + + # empty lines are preserved on explicitly set values + c.set('sec', 'longopt', '\n'.join(['a', 'b', '', 'c', '', '', 'd'])) + c.set('DEFAULT', 'NH', '\nlive free\n\nor die') + self.assertEqual(c.get('sec', 'longopt').split('\n'), + ['a', 'b', '', 'c', '', '', 'd']) + self.assertEqual(c.get('sec', 'NH').split('\n'), + ['', 'live free', '', 'or die']) + self.assertEqual(c.defaults(), { + 'poet': 'e e\ncummings', + 'nh': '\nlive free\n\nor die', + 'pi': '3.1415', + 'three': '3', + }) + # items() + l = c.items('sec') + l.sort() + self.assertEqual(l, [ + ('longopt', 'a\nb\n\nc\n\n\nd'), + ('nh', '\nlive free\n\nor die'), + ('no-three', 'one\ntwo\nfour'), + ('opt', '6'), + ('pi', '3.1415'), + ('poet', 'e e\ncummings'), + ('three', '3.0'), + ]) + + # empty line special magic goes away after remove_option() + self.assertEqual(c.get('sec', 'no-three').split('\n'), + ['one', 'two','four']) + c.remove_option('sec', 'no-three') + c.set('sec', 'no-three', 'q\n\nw') + self.assertEqual(c.get('sec', 'no-three'), 'q\n\nw') + c.remove_option('sec', 'no-three') + + def do_configparser_test(self, cfg_class): + c = cfg_class() + c.readfp(StringIO(self.s)) + self.do_test(c) + o = StringIO() + c.write(o) + self.assertEqual(o.getvalue().split('\n'), [ + '[DEFAULT]', + 'poet = e e', + '\tcummings', + 'nh = ', + '\tlive free', + '\t', + '\tor die', + 'pi = 3.1415', + 'three = 3', + '', + '[sec]', + 'opt = 6', + 'longopt = a', + '\tb', + '\t', + '\tc', + '\t', + '\t', + '\td', + 'three = 3.0', + '', + '']) + + def test_py_rawcfg(self): + self.do_configparser_test(ConfigParser.RawConfigParser) + + def test_py_cfg(self): + self.do_configparser_test(ConfigParser.ConfigParser) + + def test_py_safecfg(self): + self.do_configparser_test(ConfigParser.SafeConfigParser) + + def do_compat_test(self, cfg_class): + c = cfg_class() + c.readfp(StringIO(self.s)) + self.do_test(c) + o = StringIO() + c.write(o) + self.assertEqual(o.getvalue().split('\n'), [ + '[DEFAULT]', + 'pi = 3.1415', + 'three = 3', + 'poet = e e', + '', + ' cummings', + 'NH =', + ' live free', + '', + ' or die', + '', + '[sec]', + 'opt = 6', + 'three = 3.0', + 'longopt = a', + ' b', + '', + ' c', + '', + '', + ' d', + '', + '']) + + def test_py_rawcfg(self): + self.do_compat_test(compat.RawConfigParser) + + def test_py_cfg(self): + self.do_compat_test(compat.ConfigParser) + + def test_py_safecfg(self): + self.do_compat_test(compat.SafeConfigParser) + +class test_pickle(unittest.TestCase): + s = dedent("""\ + [DEFAULT] + pi = 3.1415 + three = 3 + poet = e e + + cummings + NH = + live free + + or die + + [sec] + opt = 6 + three = 3.0 + no-three = one + two + + four + + james = bond + """) + + def do_compat_checks(self, c): + self.assertEqual(c.sections(), ['sec']) + self.assertEqual(sorted(c.options('sec')), + ['james', 'nh', 'no-three', 'opt', 'pi', 'poet', 'three']) + self.assertEqual(c.defaults(), { + 'poet': 'e e\ncummings', + 'nh': '\nlive free\nor die', + 'pi': '3.1415', + 'three': '3', + }) + l = c.items('sec') + l.sort() + self.assertEqual(l, [ + ('james', 'bond'), + ('nh', '\nlive free\nor die'), + ('no-three', 'one\ntwo\nfour'), + ('opt', '6'), + ('pi', '3.1415'), + ('poet', 'e e\ncummings'), + ('three', '3.0'), + ]) + self.do_ini_checks(c.data) + + def do_ini_checks(self, c): + self.assertEqual(list(c), ['sec']) + self.assertEqual(sorted(c['sec']), ['james', 'nh', 'no-three', 'opt', 'pi', 'poet', 'three']) + self.assertEqual(c._defaults['pi'], '3.1415') + self.assertEqual(c.sec.opt, '6') + self.assertEqual(c.sec.three, '3.0') + self.assertEqual(c.sec['no-three'], 'one\ntwo\n\nfour') + self.assertEqual(c.sec.james, 'bond') + self.assertEqual(c.sec.pi, '3.1415') + self.assertEqual(c.sec.poet, 'e e\n\ncummings') + self.assertEqual(c.sec.NH, '\nlive free\n\nor die') + self.assertEqual(str(c), self.s) + + def test_compat(self): + for cfg_class in (compat.ConfigParser, compat.RawConfigParser, compat.SafeConfigParser): + c = cfg_class() + c.readfp(StringIO(self.s)) + self.do_compat_checks(c) + p = pickle.dumps(c) + c = None + c2 = pickle.loads(p) + self.do_compat_checks(c2) + + def test_ini(self): + c = ini.INIConfig() + c._readfp(StringIO(self.s)) + self.do_ini_checks(c) + p = pickle.dumps(c) + c = None + c2 = pickle.loads(p) + self.do_ini_checks(c2) + +class suite(unittest.TestSuite): + def __init__(self): + unittest.TestSuite.__init__(self, [ + unittest.makeSuite(test_optionxform_override, 'test'), + unittest.makeSuite(test_readline, 'test'), + unittest.makeSuite(test_multiline_with_comments, 'test'), + unittest.makeSuite(test_empty_file, 'test'), + unittest.makeSuite(test_custom_dict, 'test'), + unittest.makeSuite(test_compat, 'test'), + unittest.makeSuite(test_pickle, 'test'), + ])
\ No newline at end of file diff --git a/tests/test_unicode.py b/tests/test_unicode.py new file mode 100644 index 0000000..a56fcab --- /dev/null +++ b/tests/test_unicode.py @@ -0,0 +1,48 @@ +import unittest +from StringIO import StringIO +from iniparse import compat, ini + +class test_unicode(unittest.TestCase): + """Test files read in unicode-mode.""" + + s1 = u"""\ +[foo] +bar = fish + """ + + s2 = u"""\ +\ufeff[foo] +bar = mammal +baz = Marc-Andr\202 + """ + + def basic_tests(self, s, strable): + f = StringIO(s) + i = ini.INIConfig(f) + self.assertEqual(unicode(i), s) + self.assertEqual(type(i.foo.bar), unicode) + if strable: + self.assertEqual(str(i), str(s)) + else: + self.assertRaises(UnicodeEncodeError, lambda: str(i)) + return i + + def test_ascii(self): + i = self.basic_tests(self.s1, strable=True) + self.assertEqual(i.foo.bar, 'fish') + + def test_unicode_without_bom(self): + i = self.basic_tests(self.s2[1:], strable=False) + self.assertEqual(i.foo.bar, 'mammal') + self.assertEqual(i.foo.baz, u'Marc-Andr\202') + + def test_unicode_with_bom(self): + i = self.basic_tests(self.s2, strable=False) + self.assertEqual(i.foo.bar, 'mammal') + self.assertEqual(i.foo.baz, u'Marc-Andr\202') + +class suite(unittest.TestSuite): + def __init__(self): + unittest.TestSuite.__init__(self, [ + unittest.makeSuite(test_unicode, 'test'), + ]) |