Using ConfigParser to read a file without section name
Asked Answered
N

8

120

I am using ConfigParser to read the runtime configuration of a script.

I would like to have the flexibility of not providing a section name (there are scripts which are simple enough; they don't need a 'section'). ConfigParser will throw a NoSectionError exception, and will not accept the file.

How can I make ConfigParser simply retrieve the (key, value) tuples of a config file without section names?

For instance:

key1=val1
key2:val2

I would rather not write to the config file.

Narcotic answered 21/5, 2010 at 20:1 Comment(1)
Possible duplicate of parsing .properties file in PythonSapphirine
M
58

Alex Martelli provided a solution for using ConfigParser to parse .properties files (which are apparently section-less config files).

His solution is a file-like wrapper that will automagically insert a dummy section heading to satisfy ConfigParser's requirements.

Metcalf answered 21/5, 2010 at 21:37 Comment(2)
+1 because that is exactly what I was about to suggest. Why add all the complexity when all you have to do is just add a section!Strychnine
@jathanism: there are cases where you want to work with existing config/properties files, which are read by existing Java code and you don't know the risk of modifying those headersDiandiana
P
66

You can do this in a single line of code.

(But tomllib is probably a better choice. See the bottom of my answer.)

In python 3, prepend a fake section header to your config file data, and pass it to read_string().

from configparser import ConfigParser

parser = ConfigParser()
with open("foo.conf") as stream:
    parser.read_string("[top]\n" + stream.read())  # This line does the trick.

You could also use itertools.chain() to simulate a section header for read_file(). This might be more memory-efficient than the above approach, which might be helpful if you have large config files in a constrained runtime environment.

from configparser import ConfigParser
from itertools import chain

parser = ConfigParser()
with open("foo.conf") as lines:
    lines = chain(("[top]",), lines)  # This line does the trick.
    parser.read_file(lines)

In python 2, prepend a fake section header to your config file data, wrap the result in a StringIO object, and pass it to readfp().

from ConfigParser import ConfigParser
from StringIO import StringIO

parser = ConfigParser()
with open("foo.conf") as stream:
    stream = StringIO("[top]\n" + stream.read())  # This line does the trick.
    parser.readfp(stream)

With any of these approaches, your config settings will be available in parser.items('top').

You could use StringIO in python 3 as well, perhaps for compatibility with both old and new python interpreters, but note that it now lives in the io package and readfp() is now deprecated.

Alternatively, you might consider using a TOML parser instead of ConfigParser. Python 3.11 added a parser to the standard library: tomllib

Peggie answered 11/11, 2014 at 7:47 Comment(10)
In the itertools.chain() example, why are you using a tuple and not a list, like this: lines = chain(["[top]"], lines)? Besides the somewhat unusual one-element tuple syntax, which may be unfamilliar to some people, using a list is also 1 character shorter and looks neater without the (required) dangling comma IMHO.Criswell
I'm in the habit of using tuples for sequences that will never change, because they're more efficient, less prone to error, and better at conveying the intent than lists. If someone has trouble understanding python's tuple syntax or believes brevity is more important than clarity, they can use a list instead.Caskey
great workaround, but this seems like it ought to "just work" when you set the default_section option in the ConfigParser constructor.Atomic
@Atomic The default section is a section, with a name. The question is about reading a file without one.Caskey
@Peggie so there is an interpretation of the meaning of 'default_section' whereby if there is no section in the file it should be presumed to be the default section, allowing the processing of that file with no section.Atomic
@Atomic Oh, I see: You were talking about what the English words might imply. Yes, that's true. Unfortunately, that's not what default_section means in the context of this API.Caskey
@Peggie yes, as stated in my initial comment, " ... this seems like it ought to "just work" when you...". I'm sorry, this isn't a disagreement. It is an externally facing parameter that maps to a general meaning we all know, and also maps to some specific internal meaning which is different. You've agreed to the first and asserted the second (which I agree to also: setting the default_section does not behave that way), so there is simply no disagreement. Semantically, the default_section ought to allow this behavior -- my position stands.Atomic
@Atomic The question of your meaning comes from the context of it appearing as a response to my answer, which is about things as they are, not as they could have been, and the fact that that there is another interpretation of the words in question. Perhaps naming it "defaults section" instead of "default section" would have avoided the ambiguity.Caskey
@Peggie yeah, I am uncovering whether or not to use the library, and point is you typically do not use INI files from python, but you often might want to manipulate them in some automatic process. So this library might just be broken, as I had to use it as a line processor instead and roll my own INI parser to avoid blowing out the user's (who requested the tool) comments (in addition to this problem). It actually made using python more problematic than AWK or BASH -- which I went with instead. Ideally, I'd have an original parser implemented in py, but this library being "The Solution"(1/2)Atomic
@Peggie for python is blocking typical use cases -- which this question addresses. The point I was making was editorial: this tool Should handle the case of a missing section header (and, unrelated to this thread, comments) in a better manner. Or at least one consistent with the names they use for parameters. Although I did create a python INI parser with CRUD functionality, I can't open source it. But it is more complicated than what I did in BASH, with only U functionality. Unf. we are going w/ BASH solution because this libs choices made a real world implementation too complicated.Atomic
M
58

Alex Martelli provided a solution for using ConfigParser to parse .properties files (which are apparently section-less config files).

His solution is a file-like wrapper that will automagically insert a dummy section heading to satisfy ConfigParser's requirements.

Metcalf answered 21/5, 2010 at 21:37 Comment(2)
+1 because that is exactly what I was about to suggest. Why add all the complexity when all you have to do is just add a section!Strychnine
@jathanism: there are cases where you want to work with existing config/properties files, which are read by existing Java code and you don't know the risk of modifying those headersDiandiana
T
53

Enlightened by this answer by jterrace, I come up with this solution:

  1. Read entire file into a string
  2. Prefix with a default section name
  3. Use StringIO to mimic a file-like object
ini_str = '[root]\n' + open(ini_path, 'r').read()
ini_fp = StringIO.StringIO(ini_str)
config = ConfigParser.RawConfigParser()
config.readfp(ini_fp)


EDIT for future googlers: As of Python 3.4+ readfp is deprecated, and StringIO is not needed anymore. Instead we can use read_string directly:

with open('config_file') as f:
    file_content = '[dummy_section]\n' + f.read()

config_parser = ConfigParser.RawConfigParser()
config_parser.read_string(file_content)
Thuggee answered 24/5, 2012 at 23:1 Comment(2)
This also works wonders to parse a simple Makefile (with only aliases)! Here is a full script to substitute aliases by their full commands in Python, inspired by this answer.Gwenn
anyone else getting ` configparser.DuplicateOptionError` ?Ingeborgingelbert
L
20

You can use the ConfigObj library to do that simply : http://www.voidspace.org.uk/python/configobj.html

Updated: Find latest code here.

If you are under Debian/Ubuntu, you can install this module using your package manager :

apt-get install python-configobj

An example of use:

from configobj import ConfigObj

config = ConfigObj('myConfigFile.ini')
config.get('key1') # You will get val1
config.get('key2') # You will get val2
Leclerc answered 27/10, 2011 at 15:18 Comment(0)
A
10

The easiest way to do this is to use python's CSV parser, in my opinion. Here's a read/write function demonstrating this approach as well as a test driver. This should work provided the values are not allowed to be multi-line. :)

import csv
import operator

def read_properties(filename):
    """ Reads a given properties file with each line of the format key=value.  Returns a dictionary containing the pairs.

    Keyword arguments:
        filename -- the name of the file to be read
    """
    result={ }
    with open(filename, "rb") as csvfile:
        reader = csv.reader(csvfile, delimiter='=', escapechar='\\', quoting=csv.QUOTE_NONE)
        for row in reader:
            if len(row) != 2:
                raise csv.Error("Too many fields on row with contents: "+str(row))
            result[row[0]] = row[1] 
    return result

def write_properties(filename,dictionary):
    """ Writes the provided dictionary in key-sorted order to a properties file with each line of the format key=value

    Keyword arguments:
        filename -- the name of the file to be written
        dictionary -- a dictionary containing the key/value pairs.
    """
    with open(filename, "wb") as csvfile:
        writer = csv.writer(csvfile, delimiter='=', escapechar='\\', quoting=csv.QUOTE_NONE)
        for key, value in sorted(dictionary.items(), key=operator.itemgetter(0)):
                writer.writerow([ key, value])

def main():
    data={
        "Hello": "5+5=10",
        "World": "Snausage",
        "Awesome": "Possum"
    }

    filename="test.properties"
    write_properties(filename,data)
    newdata=read_properties(filename)

    print "Read in: "
    print newdata
    print

    contents=""
    with open(filename, 'rb') as propfile:
        contents=propfile.read()
    print "File contents:"
    print contents

    print ["Failure!", "Success!"][data == newdata]
    return

if __name__ == '__main__': 
     main() 
Actinolite answered 22/10, 2012 at 20:26 Comment(1)
+1 Clever use of the csv module to solve common ConfigParser complaint. Easily generalized more and made to be both Python 2 & 3 compatible.Burgenland
E
6

Having ran into this problem myself, I wrote a complete wrapper to ConfigParser (the version in Python 2) that can read and write files without sections transparently, based on Alex Martelli's approach linked on the accepted answer. It should be a drop-in replacement to any usage of ConfigParser. Posting it in case anyone in need of that finds this page.

import ConfigParser
import StringIO

class SectionlessConfigParser(ConfigParser.RawConfigParser):
    """
    Extends ConfigParser to allow files without sections.

    This is done by wrapping read files and prepending them with a placeholder
    section, which defaults to '__config__'
    """

    def __init__(self, *args, **kwargs):
        default_section = kwargs.pop('default_section', None)
        ConfigParser.RawConfigParser.__init__(self, *args, **kwargs)

        self._default_section = None
        self.set_default_section(default_section or '__config__')

    def get_default_section(self):
        return self._default_section

    def set_default_section(self, section):
        self.add_section(section)

        # move all values from the previous default section to the new one
        try:
            default_section_items = self.items(self._default_section)
            self.remove_section(self._default_section)
        except ConfigParser.NoSectionError:
            pass
        else:
            for (key, value) in default_section_items:
                self.set(section, key, value)

        self._default_section = section

    def read(self, filenames):
        if isinstance(filenames, basestring):
            filenames = [filenames]

        read_ok = []
        for filename in filenames:
            try:
                with open(filename) as fp:
                    self.readfp(fp)
            except IOError:
                continue
            else:
                read_ok.append(filename)

        return read_ok

    def readfp(self, fp, *args, **kwargs):
        stream = StringIO()

        try:
            stream.name = fp.name
        except AttributeError:
            pass

        stream.write('[' + self._default_section + ']\n')
        stream.write(fp.read())
        stream.seek(0, 0)

        return ConfigParser.RawConfigParser.readfp(self, stream, *args,
                                                   **kwargs)

    def write(self, fp):
        # Write the items from the default section manually and then remove them
        # from the data. They'll be re-added later.
        try:
            default_section_items = self.items(self._default_section)
            self.remove_section(self._default_section)

            for (key, value) in default_section_items:
                fp.write("{0} = {1}\n".format(key, value))

            fp.write("\n")
        except ConfigParser.NoSectionError:
            pass

        ConfigParser.RawConfigParser.write(self, fp)

        self.add_section(self._default_section)
        for (key, value) in default_section_items:
            self.set(self._default_section, key, value)
Epilate answered 6/2, 2012 at 9:35 Comment(0)
M
5

Blueicefield's answer mentioned configobj, but the original lib only supports Python 2. It now has a Python 3+ compatible port:

https://github.com/DiffSK/configobj

APIs haven't changed, see it's doc.

Misappropriate answered 2/4, 2015 at 2:29 Comment(2)
While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From ReviewWebbing
And what if the API change? You can always edit answer if it's outdated, be it a link or code snippet. I've done it for numerous times, I don't see why you think it's a problem.Misappropriate
K
0

The development version of Python (3.13) finally support files without sections.

https://docs.python.org/3.13/library/configparser.html#unnamed-sections

Katelyn answered 1/5 at 23:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.