diff options
author | grabner <pjg.github@ubergrabner.net> | 2016-12-14 14:13:20 -0500 |
---|---|---|
committer | grabner <pjg.github@ubergrabner.net> | 2016-12-14 14:13:20 -0500 |
commit | 3181a89236c928fe7eb9da78767e5957e84ca5cc (patch) | |
tree | 8e716afdb11e9906e2faa4f6178d096b0ce60db0 | |
parent | e7f6a08a91b60b8bfc3c505ce8e14e00c5133cee (diff) | |
download | iniherit-3181a89236c928fe7eb9da78767e5957e84ca5cc.tar.gz |
added "%(SUPER)s" expansion support
-rw-r--r-- | iniherit/__init__.py | 2 | ||||
-rw-r--r-- | iniherit/interpolation.py | 37 | ||||
-rw-r--r-- | iniherit/mixin.py | 50 | ||||
-rw-r--r-- | iniherit/parser.py | 69 | ||||
-rw-r--r-- | iniherit/test.py | 98 |
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 |