confini

Parse and merge multiple ini files in python3
git clone git://git.defalsify.org/python-confini.git
Log | Files | Refs | README | LICENSE

commit aacf8f1825fb8b7f74c2551001577e1aa5f573cf
parent 162a0b6d4f0772e425dbe142e55575631e306512
Author: nolash <dev@holbrook.no>
Date:   Tue,  2 Nov 2021 07:31:17 +0100

Add config env parser, doc parser, doc addition in exports

Diffstat:
Aconfini/common.py | 2++
Mconfini/config.py | 16+++++++---------
Aconfini/doc.py | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfini/env.py | 40++++++++++++++++++++++++++++++++++++++++
Mconfini/export.py | 26+++++++++++++++++++-------
Atests/files/.confini | 2++
Atests/files/doc/ok/.confini | 2++
Atests/files/doc/ok/bar.ini | 2++
Atests/files/doc/ok/foo.ini | 3+++
Atests/files/env/env.txt | 3+++
Atests/test_doc.py | 28++++++++++++++++++++++++++++
Mtests/test_env.py | 15+++++++++++++--
Mtests/test_export.py | 30++++++++++++++++++++++++++++++
13 files changed, 213 insertions(+), 18 deletions(-)

diff --git a/confini/common.py b/confini/common.py @@ -0,0 +1,2 @@ +def to_constant_name(directive, section): + return '{}_{}'.format(section.upper(), directive.upper()) diff --git a/confini/config.py b/confini/config.py @@ -9,6 +9,9 @@ import re # external imports import gnupg +# local imports +from confini.common import to_constant_name + logg = logging.getLogger('confini') current_config = None @@ -89,7 +92,7 @@ class Config: def censor(self, identifier, section=None): constant_name = '' if section != None: - constant_name = Config.to_constant_name(identifier, section) + constant_name = to_constant_name(identifier, section) else: constant_name = identifier self.censored[constant_name] = True @@ -111,15 +114,10 @@ class Config: return True - @staticmethod - def to_constant_name(directive, section): - return '{}_{}'.format(section.upper(), directive.upper()) - - def _sections_override(self, dct, dct_description): for s in self.parser.sections(): for k in self.parser[s]: - cn = Config.to_constant_name(k, s) + cn = to_constant_name(k, s) self.override(cn, self.parser[s][k], dct, dct_description) @@ -190,7 +188,7 @@ class Config: self.parser.read(tmp_config_file_path) for s in self.parser.sections(): for so in self.parser.options(s): - k = self.to_constant_name(so, s) + k = to_constant_name(so, s) v = self.parser.get(s, so) logg.debug('default config set {}'.format(k)) self.add(v, k, exists_ok=True) @@ -201,7 +199,7 @@ class Config: local_parser.read(tmp_config_file_path) for s in local_parser.sections(): for so in local_parser.options(s): - k = self.to_constant_name(so, s) + k = to_constant_name(so, s) if not self.have(k): raise KeyError('config overrides in {} defines key {} not present in default config {}'.format(self.dirs[i], k, self.dirs[0])) v = local_parser.get(s, so) diff --git a/confini/doc.py b/confini/doc.py @@ -0,0 +1,62 @@ +# standard imports +import os +import configparser +import logging + +# local imports +from confini.common import to_constant_name + +logg = logging.getLogger(__name__) + + +class ConfigDoc: + + def __init__(self, src): + fp = os.path.join(src, '.confini') + logg.debug('attempting doc parser with src {}'.format(fp)) + + self.src = fp + self.docs = {} + self.docs_flat = {} + + try: + self.process_as_ini() + except Exception: + self.process_as_env() + + + def process_as_ini(self): + p = configparser.ConfigParser() + p.read_file(self.src) + return self.process_parser(p) + + + def process_as_env(self): + from confini.env import ConfigEnvParser + c = ConfigEnvParser() + p = c.from_file(self.src) + return self.process_parser(p) + + + def process_parser(self, p): + for ks in p.sections(): + if self.docs.get(ks) == None: + self.docs[ks] = {} + for ko in p.options(ks): + v = p.get(ks, ko) + self.docs[ks][ko] = v + c = to_constant_name(ko, ks) + self.docs_flat[c] = v + logg.debug('docs {} -> {} = {}'.format(ks, ko, v)) + + + def get(self, k, o=None): + if o == None: + return self.docs_flat[k] + else: + return self.docs[k][o] + + + @staticmethod + def from_config(config): + return ConfigDoc(config.dirs[0]) diff --git a/confini/env.py b/confini/env.py @@ -0,0 +1,40 @@ +# standard imports +import configparser +import io +import logging + +logg = logging.getLogger(__name__) + + +class ConfigEnvParser: + + def __init__(self): + self.parser = configparser.ConfigParser() + + + def from_file(self, fp): + f = open(fp, 'r') + r = self.from_handle(f) + f.close() + return r + + + def from_string(self, s): + fh = io.StringIO(s) + return self.from_handle(fh) + + + def from_handle(self, fh): + while True: + l = fh.readline() + if len(l) == 0: + break + (k, v) = l.split('=') + (ks, ko) = k.split('_', maxsplit=1) + ks = ks.lower() + ko = ko.lower() + v = v.rstrip() + if not self.parser.has_section(ks): + self.parser.add_section(ks) + self.parser.set(ks, ko, v) + return self.parser diff --git a/confini/export.py b/confini/export.py @@ -18,12 +18,14 @@ class ConfigExporterTarget(enum.Enum): class ConfigExporter: - def __init__(self, config, target=None, split=False): + def __init__(self, config, target=None, split=False, doc=False): self.config = config self.sections = {} self.target_split = split self.target_typ = ConfigExporterTarget.HANDLE self.target = None + self.make_doc = doc + self.doc = None if isinstance(target, io.IOBase): self.target = target else: @@ -36,6 +38,10 @@ class ConfigExporter: d = os.getcwd() self.target = os.path.join(d, target) + if self.make_doc: + from confini.doc import ConfigDoc + self.doc = ConfigDoc.from_config(config) + def scan(self): for k in self.config.all(): @@ -47,12 +53,18 @@ class ConfigExporter: self.sections[s][v] = self.config.get(k) - def export_section(self, k, w): - s = {} - s[k] = self.sections[k] - p = configparser.ConfigParser() - p.read_dict(s) - p.write(w) + def export_section(self, ks, w): + w.write("[" + ks + "]\n") + for ko in self.sections[ks].keys(): + if self.make_doc: + try: + v = self.doc.get(ks, ko) + w.write("# " + v + "\n") + except KeyError: + logg.warning('doc missing for section {} option {}'.format(ks, ko)) + pass + w.write(ko + " = " + self.sections[ks][ko] + "\n") + w.write("\n") def export(self): diff --git a/tests/files/.confini b/tests/files/.confini @@ -0,0 +1,2 @@ +FOO_BAR=bar of foo +XYZZY_BERT=bert of xyzzy diff --git a/tests/files/doc/ok/.confini b/tests/files/doc/ok/.confini @@ -0,0 +1,2 @@ +FOO_BAR=foo of bar +BAR_XYZZY=bar of xyzzy diff --git a/tests/files/doc/ok/bar.ini b/tests/files/doc/ok/bar.ini @@ -0,0 +1,2 @@ +[bar] +xyzzy = 666 diff --git a/tests/files/doc/ok/foo.ini b/tests/files/doc/ok/foo.ini @@ -0,0 +1,3 @@ +[foo] +bar = 42 +baz = 13 diff --git a/tests/files/env/env.txt b/tests/files/env/env.txt @@ -0,0 +1,3 @@ +FOO_BAR=13 +FOO_BAZ=42 +BAR_XYZZY=666 diff --git a/tests/test_doc.py b/tests/test_doc.py @@ -0,0 +1,28 @@ +# standard imports +import unittest +import os +import logging + +# local imports +from confini import Config +from confini.doc import ConfigDoc + +logging.basicConfig(level=logging.DEBUG) + + +class TestDoc(unittest.TestCase): + + wd = os.path.dirname(__file__) + + def test_from_config(self): + inidir = os.path.join(self.wd, 'files/doc/ok') + c = Config(inidir) + c.process() + + doc = ConfigDoc.from_config(c) + self.assertEqual(doc.get('FOO_BAR'), 'foo of bar') + self.assertEqual(doc.get('BAR_XYZZY'), 'bar of xyzzy') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_env.py b/tests/test_env.py @@ -1,9 +1,10 @@ -#!/usr/bin/python - +# standard imports import os import unittest import logging +# local imports +from confini.env import ConfigEnvParser from confini import Config logging.basicConfig(level=logging.DEBUG) @@ -84,5 +85,15 @@ class TestEnv(unittest.TestCase): } self.assertDictEqual(expect, c.store) + + def test_env_parser(self): + envpath = os.path.join(self.wd, 'files', 'env', 'env.txt') + c = ConfigEnvParser() + p = c.from_file(envpath) + self.assertTrue(p.has_section('foo')) + self.assertTrue(p.has_section('bar')) + self.assertTrue(p.has_option('foo', 'bar')) + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_export.py b/tests/test_export.py @@ -5,10 +5,12 @@ import unittest import logging import configparser import tempfile +import re # local imports from confini import Config from confini.export import ConfigExporter +from confini.doc import ConfigDoc logging.basicConfig(level=logging.DEBUG) @@ -99,5 +101,33 @@ class TestExport(unittest.TestCase): self.assertEqual(b.get(s, o), a.get(s.lower(), o.lower())) + + def test_doc(self): + w = io.StringIO() + e = ConfigExporter(self.config, target=w, doc=True) + e.export() + + w.seek(0) + s = w.read() + + re_c = re.compile('^# ', re.MULTILINE) + m = re_c.finditer(s) + next(m) + next(m) + with self.assertRaises(StopIteration): + next(m) + + a = configparser.ConfigParser() + a.read_string(s) + + b = configparser.ConfigParser() + b.read(os.path.join(self.inidir, 'foo.ini')) + b.read(os.path.join(self.inidir, 'bar.ini')) + + for s in b.sections(): + for o in b.options(s): + self.assertEqual(b.get(s, o), a.get(s.lower(), o.lower())) + + if __name__ == '__main__': unittest.main()