summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgrabner <pjg.github@ubergrabner.net>2016-12-14 14:13:20 -0500
committergrabner <pjg.github@ubergrabner.net>2016-12-14 14:13:20 -0500
commit3181a89236c928fe7eb9da78767e5957e84ca5cc (patch)
tree8e716afdb11e9906e2faa4f6178d096b0ce60db0
parente7f6a08a91b60b8bfc3c505ce8e14e00c5133cee (diff)
downloadiniherit-3181a89236c928fe7eb9da78767e5957e84ca5cc.tar.gz
added "%(SUPER)s" expansion support
-rw-r--r--iniherit/__init__.py2
-rw-r--r--iniherit/interpolation.py37
-rw-r--r--iniherit/mixin.py50
-rw-r--r--iniherit/parser.py69
-rw-r--r--iniherit/test.py98
5 files changed, 200 insertions, 56 deletions
diff --git a/iniherit/__init__.py b/iniherit/__init__.py
index bcfbafc..85a946a 100644
--- a/iniherit/__init__.py
+++ b/iniherit/__init__.py
@@ -8,7 +8,7 @@
from .parser import *
from . import mixin
-from .interpolation import InterpolationMissingEnvError
+from .interpolation import InterpolationMissingEnvError, InterpolationMissingSuperError
#------------------------------------------------------------------------------
# end of $Id$
diff --git a/iniherit/interpolation.py b/iniherit/interpolation.py
index d431b9b..1c9e2f4 100644
--- a/iniherit/interpolation.py
+++ b/iniherit/interpolation.py
@@ -13,32 +13,59 @@ from six.moves import configparser as CP
#------------------------------------------------------------------------------
class InterpolationMissingEnvError(CP.InterpolationMissingOptionError): pass
+class InterpolationMissingSuperError(CP.InterpolationMissingOptionError): pass
#------------------------------------------------------------------------------
_env_cre = re.compile(r'%\(ENV:([^:)]+)(:-([^)]*))?\)s', flags=re.DOTALL)
+_super_cre = re.compile(r'%\(SUPER(:-([^)]*))?\)s', flags=re.DOTALL)
def interpolate(parser, base_interpolate, section, option, rawval, vars):
value = rawval
depth = CP.MAX_INTERPOLATION_DEPTH
- repl = lambda match: _env_replace(
+ erepl = lambda match: _env_replace(
match, parser, base_interpolate, section, option, rawval, vars)
- while depth and _env_cre.search(value):
+ srepl = lambda match: _super_replace(
+ match, parser, parser, None, section, option, rawval, vars)
+ while depth and ( _env_cre.search(value) or _super_cre.search(value) ):
depth -= 1
- value = _env_cre.sub(repl, value)
- if _env_cre.search(value):
+ value = _env_cre.sub(erepl, value)
+ value = _super_cre.sub(srepl, value)
+ if not depth and ( _env_cre.search(value) or _super_cre.search(value) ):
raise CP.InterpolationDepthError(option, section, rawval)
if '%(SUPER)s' in value:
raise InterpolationMissingSuperError(option, section, rawval, 'SUPER')
return base_interpolate(parser, section, option, value, vars)
#------------------------------------------------------------------------------
+def interpolate_super(parser, src, dst, section, option, value):
+ srepl = lambda match: _super_replace(
+ match, parser, src, dst, section, option, value, None)
+ value = _super_cre.sub(srepl, value)
+ return value
+
+#------------------------------------------------------------------------------
def _env_replace(match, parser, base_interpolate, section, option, rawval, vars):
if match.group(1) in os.environ:
return os.environ.get(match.group(1))
- if match.group(2) is not None:
+ if match.group(2):
return match.group(3)
raise InterpolationMissingEnvError(option, section, rawval, match.group(1))
#------------------------------------------------------------------------------
+def _super_replace(match, parser, src, dst, section, option, rawval, vars):
+ if dst \
+ and ( section == parser.IM_DEFAULTSECT or dst.has_section(section) ) \
+ and dst.has_option(section, option):
+ try:
+ return dst.get(section, option, raw=True, vars=vars)
+ except TypeError:
+ return dst.get(section, option)
+ if dst:
+ return match.group(0)
+ if match.group(1):
+ return match.group(2)
+ raise InterpolationMissingSuperError(option, section, rawval, 'SUPER')
+
+#------------------------------------------------------------------------------
# end of $Id$
# $ChangeLog$
#------------------------------------------------------------------------------
diff --git a/iniherit/mixin.py b/iniherit/mixin.py
index d4963b0..15b525d 100644
--- a/iniherit/mixin.py
+++ b/iniherit/mixin.py
@@ -15,7 +15,8 @@ from .parser import IniheritMixin
# todo: should this perhaps use the `stub` package instead?...
-attrs = [attr for attr in dir(IniheritMixin) if not attr.startswith('__')]
+raw_attrs = [attr for attr in dir(IniheritMixin) if not attr.startswith('__')]
+base_attrs = ['_interpolate']
#------------------------------------------------------------------------------
def install_globally():
@@ -27,34 +28,43 @@ def install_globally():
if hasattr(CP.RawConfigParser, '_iniherit_installed_'):
return
setattr(CP.RawConfigParser, '_iniherit_installed_', True)
- for attr in attrs:
- if hasattr(CP.RawConfigParser, attr):
- setattr(CP.RawConfigParser,
- '_iniherit_' + attr, getattr(CP.RawConfigParser, attr))
- meth = getattr(IniheritMixin, attr)
- if six.callable(meth):
- if six.PY2:
- import new
- meth = new.instancemethod(meth.__func__, None, CP.RawConfigParser)
- else:
- meth = meth.__get__(None, CP.RawConfigParser)
- setattr(CP.RawConfigParser, attr, meth)
+ for target, attrs in (
+ (CP.RawConfigParser, raw_attrs),
+ (CP.ConfigParser, base_attrs),
+ ):
+ for attr in attrs:
+ if hasattr(target, attr):
+ setattr(target,
+ '_iniherit_' + attr, getattr(target, attr))
+ meth = getattr(IniheritMixin, attr)
+ if six.callable(meth):
+ if six.PY2:
+ import new
+ meth = new.instancemethod(meth.__func__, None, target)
+ else:
+ meth = meth.__get__(None, target)
+ setattr(target, attr, meth)
#------------------------------------------------------------------------------
def uninstall_globally():
'''
Reverts the effects of :func:`install_globally`.
'''
- if not hasattr(CP.ConfigParser, '_iniherit_installed_'):
+ if not hasattr(CP.RawConfigParser, '_iniherit_installed_'):
return
delattr(CP.RawConfigParser, '_iniherit_installed_')
- for attr in attrs:
- if hasattr(CP.RawConfigParser, '_iniherit_' + attr):
- xattr = getattr(CP.RawConfigParser, '_iniherit_' + attr)
- setattr(CP.RawConfigParser, attr, xattr)
- else:
- delattr(CP.RawConfigParser, attr)
+ for target, attrs in (
+ (CP.RawConfigParser, raw_attrs),
+ (CP.ConfigParser, base_attrs),
+ ):
+ for attr in attrs:
+ if hasattr(target, '_iniherit_' + attr):
+ xattr = getattr(target, '_iniherit_' + attr)
+ setattr(target, attr, xattr)
+ else:
+ delattr(target, attr)
#------------------------------------------------------------------------------
# end of $Id$
+# $ChangeLog$
#------------------------------------------------------------------------------
diff --git a/iniherit/parser.py b/iniherit/parser.py
index ed5c5be..e883334 100644
--- a/iniherit/parser.py
+++ b/iniherit/parser.py
@@ -103,12 +103,12 @@ class IniheritMixin(object):
#----------------------------------------------------------------------------
def _readRecursive(self, fp, fpname, encoding=None):
ret = self._makeParser()
- ret.readfp(fp, fpname)
+ src = self._makeParser()
+ src.readfp(fp, fpname)
dirname = os.path.dirname(fpname)
- if ret.has_option(self.IM_DEFAULTSECT, self.IM_INHERITTAG):
- inilist = ret.get(self.IM_DEFAULTSECT, self.IM_INHERITTAG)
- ret.remove_option(self.IM_DEFAULTSECT, self.IM_INHERITTAG)
- tmp = self._makeParser()
+ if src.has_option(self.IM_DEFAULTSECT, self.IM_INHERITTAG):
+ inilist = src.get(self.IM_DEFAULTSECT, self.IM_INHERITTAG)
+ src.remove_option(self.IM_DEFAULTSECT, self.IM_INHERITTAG)
for curname in inilist.split():
optional = curname.startswith('?')
if optional:
@@ -120,15 +120,12 @@ class IniheritMixin(object):
if optional:
continue
raise
- self._apply(self._readRecursive(curfp, curname, encoding=encoding), tmp)
- self._apply(ret, tmp)
- ret = tmp
- for section in ret.sections():
- if not ret.has_option(section, self.IM_INHERITTAG):
+ self._apply(self._readRecursive(curfp, curname, encoding=encoding), ret)
+ for section in src.sections():
+ if not src.has_option(section, self.IM_INHERITTAG):
continue
- inilist = ret.get(section, self.IM_INHERITTAG)
- ret.remove_option(section, self.IM_INHERITTAG)
- retorig = dict(ret.items(section))
+ inilist = src.get(section, self.IM_INHERITTAG)
+ src.remove_option(section, self.IM_INHERITTAG)
for curname in inilist.split():
optional = curname.startswith('?')
if optional:
@@ -146,8 +143,7 @@ class IniheritMixin(object):
raise
self._apply(self._readRecursive(curfp, curname, encoding=encoding), ret,
sections={fromsect: section})
- for k, v in retorig.items():
- _real_RawConfigParser.set(ret, section, k, v)
+ self._apply(src, ret)
return ret
#----------------------------------------------------------------------------
@@ -156,37 +152,50 @@ class IniheritMixin(object):
# the default section with the exact same value... ugh.
if sections is None:
for option, value in src.items(self.IM_DEFAULTSECT):
- _real_RawConfigParser.set(dst, self.IM_DEFAULTSECT, option, value)
+ value = interpolation.interpolate_super(
+ self, src, dst, self.IM_DEFAULTSECT, option, value)
+ self._im_setraw(dst, self.IM_DEFAULTSECT, option, value)
if sections is None:
sections = OrderedDict([(s, s) for s in src.sections()])
for srcsect, dstsect in sections.items():
if not dst.has_section(dstsect):
dst.add_section(dstsect)
for option, value in src.items(srcsect):
+ # todo: this is a *terrible* way of detecting if this option is
+ # defaulting...
if src.has_option(self.IM_DEFAULTSECT, option) \
and value == src.get(self.IM_DEFAULTSECT, option):
continue
- if six.PY3 and hasattr(dst, '_interpolation'):
- # todo: don't do this for systems that have
- # http://bugs.python.org/issue21265 fixed
- try:
- tmp = dst._interpolation.before_set
- dst._interpolation.before_set = lambda self,s,o,v,*a,**k: v
- _real_RawConfigParser.set(dst, dstsect, option, value)
- finally:
- dst._interpolation.before_set = tmp
- else:
- _real_RawConfigParser.set(dst, dstsect, option, value)
+ value = interpolation.interpolate_super(
+ self, src, dst, dstsect, option, value)
+ self._im_setraw(dst, dstsect, option, value)
+
+ #----------------------------------------------------------------------------
+ def _im_setraw(self, parser, section, option, value):
+ if six.PY3 and hasattr(dst, '_interpolation'):
+ # todo: don't do this for systems that have
+ # http://bugs.python.org/issue21265 fixed
+ try:
+ tmp = parser._interpolation.before_set
+ parser._interpolation.before_set = lambda self,s,o,v,*a,**k: v
+ _real_RawConfigParser.set(parser, section, option, value)
+ finally:
+ parser._interpolation.before_set = tmp
+ else:
+ _real_RawConfigParser.set(parser, section, option, value)
#----------------------------------------------------------------------------
# todo: yikes! overriding a private method!...
def _interpolate(self, section, option, rawval, vars):
+ base_interpolate = getattr(_real_ConfigParser, '_iniherit__interpolate', None)
+ if not base_interpolate:
+ base_interpolate = getattr(_real_ConfigParser, '_interpolate', None)
return interpolation.interpolate(
- self, _real_ConfigParser._interpolate, section, option, rawval, vars)
+ self, base_interpolate, section, option, rawval, vars)
if not hasattr(_real_ConfigParser, '_interpolate'):
warnings.warn(
- 'ConfigParser did not have a "_interpolate" method...'
- ' iniherit may be broken on this platform',
+ 'ConfigParser did not have a "_interpolate" method'
+ ' -- iniherit may be broken on this platform',
RuntimeWarning)
diff --git a/iniherit/test.py b/iniherit/test.py
index fef1bbd..826d958 100644
--- a/iniherit/test.py
+++ b/iniherit/test.py
@@ -214,6 +214,104 @@ class TestIniherit(unittest.TestCase):
'[s3]\ns3v = b3\n\n[s2]\ns2v = o2\n\n[s1]\ns1v = b1\n\n')
#----------------------------------------------------------------------------
+ def test_interpolation_super_depth(self):
+ files = {k: textwrap.dedent(v) for k, v in {
+ 'base.ini' : '''\
+ [DEFAULT]
+ keys2 = base-vals
+ # [loggers]
+ # keys = root, authz
+ # okeys = okeys-bVal
+ ''',
+ 'mid.ini' : '''\
+ [DEFAULT]
+ %inherit = base.ini ?no-such-ini.ini
+ # key1 = val1
+ ''',
+ 'extend.ini' : '''\
+ [DEFAULT]
+ %inherit = mid.ini
+ # nkeys = %(SUPER:-nval0)s, eVal1
+ keys2 = %(SUPER:-nval0)s, eVal1
+ # [loggers]
+ # keys = %(SUPER)s, authn
+ # okeys = %(SUPER:-okeys-eDef)s, okeys-eVal
+ # dkeys = %(SUPER:-dkeys-eDef)s, dkeys-eVal
+ ''',
+ }.items()}
+ parser = ConfigParser(loader=ByteLoader(files))
+ parser.read('extend.ini')
+
+ # self.assertEqual(parser.get('loggers', 'keys'), 'root, authz, authn')
+ # self.assertEqual(parser.get('loggers', 'okeys'), 'okeys-bVal, okeys-eVal')
+ # self.assertEqual(parser.get('loggers', 'dkeys'), 'dkeys-eDef, dkeys-eVal')
+
+ self.assertEqual(parser.get('DEFAULT', 'keys2'), 'base-vals, eVal1')
+
+ # self.assertEqual(parser.get('DEFAULT', 'nkeys'), 'nval0, eVal1')
+ # self.assertEqual(parser.get('DEFAULT', 'key1'), 'val1')
+
+ #----------------------------------------------------------------------------
+ def test_interpolation_super_breadth(self):
+ from iniherit import InterpolationMissingSuperError
+ files = {k: textwrap.dedent(v) for k, v in {
+ 'base.ini' : '''\
+ [loggers]
+ keys = root, authz
+ ''',
+ 'adjust.ini' : '''\
+ [loggers]
+ keys = %(SUPER)s, authn
+ nkey = %(SUPER)s and boom!
+ dkey = %(SUPER:-more)s or less
+ ''',
+ 'extend.ini' : '''\
+ [DEFAULT]
+ %inherit = base.ini adjust.ini
+ ''',
+ }.items()}
+ parser = ConfigParser(loader=ByteLoader(files))
+ parser.read('extend.ini')
+ self.assertEqual(parser.get('loggers', 'keys'), 'root, authz, authn')
+ self.assertEqual(parser.get('loggers', 'dkey'), 'more or less')
+ with self.assertRaises(InterpolationMissingSuperError) as cm:
+ parser.get('loggers', 'nkey')
+ self.assertMultiLineEqual(str(cm.exception), textwrap.dedent('''\
+ Bad value substitution:
+ \tsection: [loggers]
+ \toption : nkey
+ \tkey : SUPER
+ \trawval : %(SUPER)s and boom!
+ '''))
+
+ #----------------------------------------------------------------------------
+ def test_interpolation_super_invalid(self):
+ from iniherit import InterpolationMissingSuperError
+ files = {k: textwrap.dedent(v) for k, v in {
+ 'base.ini' : '''\
+ [DEFAULT]
+ key1 = val1
+ ''',
+ 'extend.ini' : '''\
+ [DEFAULT]
+ %inherit = base.ini
+ key2 = %(SUPER)s and boom!
+ ''',
+ }.items()}
+ files = {k: textwrap.dedent(v) for k, v in files.items()}
+ parser = ConfigParser(loader=ByteLoader(files))
+ parser.read('extend.ini')
+ with self.assertRaises(InterpolationMissingSuperError) as cm:
+ parser.get('DEFAULT', 'key2')
+ self.assertMultiLineEqual(str(cm.exception), textwrap.dedent('''\
+ Bad value substitution:
+ \tsection: [DEFAULT]
+ \toption : key2
+ \tkey : SUPER
+ \trawval : %(SUPER)s and boom!
+ '''))
+
+ #----------------------------------------------------------------------------
def test_interpolation_env(self):
import os
from six.moves.configparser import InterpolationDepthError