summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLudovico Cavedon <ludovico.cavedon@gmail.com>2009-10-20 23:52:30 -0700
committerLudovico Cavedon <ludovico.cavedon@gmail.com>2009-10-20 23:52:30 -0700
commit8cbb16c4830d341deb19d77d8ff9c10813460e46 (patch)
treecf55736336137013468fc6a4a3add54e74f45f74
Imported Upstream version 0.3.1
-rw-r--r--Changelog85
-rw-r--r--LICENSE30
-rw-r--r--LICENSE-PSF262
-rw-r--r--MANIFEST.in7
-rw-r--r--Makefile19
-rw-r--r--PKG-INFO24
-rw-r--r--README28
-rwxr-xr-xhtml/index.html170
-rw-r--r--html/style.css123
-rw-r--r--iniparse/__init__.py13
-rw-r--r--iniparse/compat.py351
-rw-r--r--iniparse/config.py273
-rw-r--r--iniparse/ini.py630
-rw-r--r--python-iniparse.spec72
-rwxr-xr-xruntests.py12
-rw-r--r--setup.py41
-rw-r--r--tests/__init__.py21
-rw-r--r--tests/test_compat.py497
-rw-r--r--tests/test_fuzz.py111
-rw-r--r--tests/test_ini.py391
-rw-r--r--tests/test_misc.py411
-rw-r--r--tests/test_unicode.py48
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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..bfe066d
--- /dev/null
+++ b/LICENSE
@@ -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
diff --git a/README b/README
new file mode 100644
index 0000000..66cd38d
--- /dev/null
+++ b/README
@@ -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 &amp;
+ 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>
+&gt;&gt;&gt; from iniparse import INIConfig
+&gt;&gt;&gt; cfg = INIConfig(file('options.ini'))
+</pre>
+ </li>
+ <li>Access/Modify data:
+<pre>
+&gt;&gt;&gt; print cfg.playlist.expand_playlist
+True
+&gt;&gt;&gt; print cfg.ui.width
+150
+&gt;&gt;&gt; cfg.ui.width = 200
+&gt;&gt;&gt; print cfg['ui']['width']
+200
+</pre>
+ </li>
+ <li>Print data:
+<pre>
+&gt;&gt;&gt; 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>
+&gt;&gt;&gt; from iniparse import ConfigParser
+&gt;&gt;&gt; cfgpr = ConfigParser()
+&gt;&gt;&gt; cfgpr.read('options.ini')
+&gt;&gt;&gt; print cfgpr.get('ui', 'width')
+150
+&gt;&gt;&gt; cfgpr.set('ui', 'width', 175)
+</pre>
+ </li>
+ <li>The new API can also be accessed via backward-compatible objects:
+<pre>
+&gt;&gt;&gt; print cfgpr.data.playlist.expand_playlist
+True
+&gt;&gt;&gt; cfgpr.data.ui.width = 200
+&gt;&gt;&gt; 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>
+&gt;&gt;&gt; from iniparse import BasicConfig
+&gt;&gt;&gt; n = BasicConfig()
+&gt;&gt;&gt; n.x = 7
+&gt;&gt;&gt; n.name.first = 'paramjit'
+&gt;&gt;&gt; n.name.last = 'oberoi'
+&gt;&gt;&gt; print n.x
+7
+&gt;&gt;&gt; print n.name.first
+'paramjit'
+&gt;&gt;&gt; print n
+name.first = paramjit
+name.last = oberoi
+x = 7
+</pre>
+ </li>
+ <li>Convert to INI:
+<pre>
+&gt;&gt;&gt; from iniparse import INIConfig
+&gt;&gt;&gt; i = INIConfig()
+&gt;&gt;&gt; del n.x # since INI doesn't support top-level values
+&gt;&gt;&gt; i.import_config(n)
+&gt;&gt;&gt; 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'),
+ ])