ui: introduce new config parser
authorMatt Mackall <mpm@selenic.com>
Thu, 23 Apr 2009 15:40:10 -0500
changeset 8144 fca54469480e
parent 8143 507c49e297e1
child 8145 0c2ba48415c8
ui: introduce new config parser
mercurial/config.py
mercurial/dispatch.py
mercurial/error.py
mercurial/ui.py
tests/test-hgrc.out
tests/test-trusted.py
tests/test-trusted.py.out
tests/test-ui-config
tests/test-ui-config.out
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/config.py	Thu Apr 23 15:40:10 2009 -0500
@@ -0,0 +1,93 @@
+from i18n import _
+import re, error
+
+class sortdict(dict):
+    'a simple append-only sorted dictionary'
+    def __init__(self, data=None):
+        self._list = []
+        if data:
+            if hasattr(data, '_list'):
+                self._list = list(data._list)
+            self.update(data)
+    def copy(self):
+        return sortdict(self)
+    def __setitem__(self, key, val):
+        if key in self:
+            self._list.remove(key)
+        self._list.append(key)
+        dict.__setitem__(self, key, val)
+    def __iter__(self):
+        return self._list.__iter__()
+    def update(self, src):
+        for k in src:
+            self[k] = src[k]
+    def items(self):
+        return [(k,self[k]) for k in self._list]
+
+class config:
+    def __init__(self, data=None):
+        self._data = {}
+        if data:
+            for k in data._data:
+                self._data[k] = data[k].copy()
+    def copy(self):
+        return config(self)
+    def __contains__(self, section):
+        return section in self._data
+    def update(self, src, sections=None):
+        if not sections:
+            sections = src.sections()
+        for s in sections:
+            if s not in src:
+                continue
+            if s not in self:
+                self._data[s] = sortdict()
+            for k in src._data[s]:
+                self._data[s][k] = src._data[s][k]
+    def get(self, section, item, default=None):
+        return self._data.get(section, {}).get(item, (default, ""))[0]
+    def getsource(self, section, item):
+        return self._data.get(section, {}).get(item, (None, ""))[1]
+    def sections(self):
+        return sorted(self._data.keys())
+    def items(self, section):
+        return [(k, v[0]) for k,v in self._data.get(section, {}).items()]
+    def set(self, section, item, value, source=""):
+        if section not in self:
+            self._data[section] = sortdict()
+        self._data[section][item] = (value, source)
+
+    def read(self, path, fp):
+        sectionre = re.compile(r'\[([^\[]+)\]')
+        itemre = re.compile(r'([^=\s]+)\s*=\s*(.*)')
+        contre = re.compile(r'\s+(\S.*)')
+        emptyre = re.compile(r'(;|#|\s*$)')
+        section = ""
+        item = None
+        line = 0
+        cont = 0
+        for l in fp:
+            line += 1
+            if cont:
+                m = contre.match(l)
+                if m:
+                    v = self.get(section, item) + "\n" + m.group(1)
+                    self.set(section, item, v, "%s:%d" % (path, line))
+                    continue
+                item = None
+            if emptyre.match(l):
+                continue
+            m = sectionre.match(l)
+            if m:
+                section = m.group(1)
+                if section not in self:
+                    self._data[section] = sortdict()
+                continue
+            m = itemre.match(l)
+            if m:
+                item = m.group(1)
+                self.set(section, item, m.group(2), "%s:%d" % (path, line))
+                cont = 1
+                continue
+            raise error.ConfigError(_('config error at %s:%d: \'%s\'')
+                                    % (path, line, l.rstrip()))
--- a/mercurial/dispatch.py	Thu Apr 23 15:40:10 2009 -0500
+++ b/mercurial/dispatch.py	Thu Apr 23 15:40:10 2009 -0500
@@ -55,6 +55,8 @@
     except error.AmbiguousCommand, inst:
         ui.warn(_("hg: command '%s' is ambiguous:\n    %s\n") %
                 (inst.args[0], " ".join(inst.args[1])))
+    except error.ConfigError, inst:
+        ui.warn(_("hg: %s\n") % inst.args[0])
     except error.LockHeld, inst:
         if inst.errno == errno.ETIMEDOUT:
             reason = _('timed out waiting for lock held by %s') % inst.locker
--- a/mercurial/error.py	Thu Apr 23 15:40:10 2009 -0500
+++ b/mercurial/error.py	Thu Apr 23 15:40:10 2009 -0500
@@ -28,6 +28,9 @@
 class ParseError(Exception):
     """Exception raised on errors in parsing the command line."""
 
+class ConfigError(Exception):
+    'Exception raised when parsing config files'
+
 class RepoError(Exception):
     pass
 
--- a/mercurial/ui.py	Thu Apr 23 15:40:10 2009 -0500
+++ b/mercurial/ui.py	Thu Apr 23 15:40:10 2009 -0500
@@ -7,27 +7,19 @@
 
 from i18n import _
 import errno, getpass, os, re, socket, sys, tempfile
-import ConfigParser, traceback, util
+import config, traceback, util, error
 
-def updateconfig(source, dest, sections=None):
-    if not sections:
-        sections = source.sections()
-    for section in sections:
-        if not dest.has_section(section):
-            dest.add_section(section)
-        if not source.has_section(section):
-            continue
-        for name, value in source.items(section, raw=True):
-            dest.set(section, name, value)
+_booleans = {'1':True, 'yes':True, 'true':True, 'on':True,
+             '0':False, 'no':False, 'false':False, 'off':False}
 
 class ui(object):
     def __init__(self, parentui=None):
         self.buffers = []
         self.quiet = self.verbose = self.debugflag = self.traceback = False
         self.interactive = self.report_untrusted = True
-        self.overlay = util.configparser()
-        self.cdata = util.configparser()
-        self.ucdata = util.configparser()
+        self.overlay = config.config()
+        self.cdata = config.config()
+        self.ucdata = config.config()
         self.parentui = None
         self.trusted_users = {}
         self.trusted_groups = {}
@@ -35,10 +27,10 @@
         if parentui:
             # parentui may point to an ui object which is already a child
             self.parentui = parentui.parentui or parentui
-            updateconfig(self.parentui.cdata, self.cdata)
-            updateconfig(self.parentui.ucdata, self.ucdata)
+            self.cdata.update(self.parentui.cdata)
+            self.ucdata.update(self.parentui.ucdata)
             # we want the overlay from the parent, not the root
-            updateconfig(parentui.overlay, self.overlay)
+            self.overlay.update(parentui.overlay)
             self.buffers = parentui.buffers
             self.trusted_users = parentui.trusted_users.copy()
             self.trusted_groups = parentui.trusted_groups.copy()
@@ -89,22 +81,21 @@
                 return
             raise
 
-        cdata = util.configparser()
+        cdata = config.config()
         trusted = sections or assumetrusted or self._is_trusted(fp, filename)
 
         try:
-            cdata.readfp(fp, filename)
-        except ConfigParser.ParsingError, inst:
-            msg = _("Failed to parse %s\n%s") % (filename, inst)
+            cdata.read(filename, fp)
+        except error.ConfigError, inst:
             if trusted:
-                raise util.Abort(msg)
-            self.warn(_("Ignored: %s\n") % msg)
+                raise
+            self.warn(_("Ignored: %s\n") % str(inst))
 
         if trusted:
-            updateconfig(cdata, self.cdata, sections)
-            updateconfig(self.overlay, self.cdata, sections)
-        updateconfig(cdata, self.ucdata, sections)
-        updateconfig(self.overlay, self.ucdata, sections)
+            self.cdata.update(cdata, sections)
+            self.cdata.update(self.overlay, sections)
+        self.ucdata.update(cdata, sections)
+        self.ucdata.update(self.overlay, sections)
 
         if root is None:
             root = os.path.expanduser('~')
@@ -117,7 +108,7 @@
                 root = os.getcwd()
             items = section and [(name, value)] or []
             for cdata in self.cdata, self.ucdata, self.overlay:
-                if not items and cdata.has_section('paths'):
+                if not items and 'paths' in cdata:
                     pathsitems = cdata.items('paths')
                 else:
                     pathsitems = items
@@ -149,8 +140,6 @@
 
     def setconfig(self, section, name, value):
         for cdata in (self.overlay, self.cdata, self.ucdata):
-            if not cdata.has_section(section):
-                cdata.add_section(section)
             cdata.set(section, name, value)
         self.fixconfig(section, name, value)
 
@@ -159,37 +148,23 @@
             return self.ucdata
         return self.cdata
 
-    def _config(self, section, name, default, funcname, untrusted, abort):
-        cdata = self._get_cdata(untrusted)
-        if cdata.has_option(section, name):
-            try:
-                func = getattr(cdata, funcname)
-                return func(section, name)
-            except (ConfigParser.InterpolationError, ValueError), inst:
-                msg = _("Error in configuration section [%s] "
-                        "parameter '%s':\n%s") % (section, name, inst)
-                if abort:
-                    raise util.Abort(msg)
-                self.warn(_("Ignored: %s\n") % msg)
-        return default
-
-    def _configcommon(self, section, name, default, funcname, untrusted):
-        value = self._config(section, name, default, funcname,
-                             untrusted, abort=True)
+    def config(self, section, name, default=None, untrusted=False):
+        value = self._get_cdata(untrusted).get(section, name, default)
         if self.debugflag and not untrusted:
-            uvalue = self._config(section, name, None, funcname,
-                                  untrusted=True, abort=False)
+            uvalue = self.ucdata.get(section, name)
             if uvalue is not None and uvalue != value:
                 self.warn(_("Ignoring untrusted configuration option "
                             "%s.%s = %s\n") % (section, name, uvalue))
         return value
 
-    def config(self, section, name, default=None, untrusted=False):
-        return self._configcommon(section, name, default, 'get', untrusted)
-
     def configbool(self, section, name, default=False, untrusted=False):
-        return self._configcommon(section, name, default, 'getboolean',
-                                  untrusted)
+        v = self.config(section, name, None, untrusted)
+        if v == None:
+            return default
+        if v.lower() not in _booleans:
+            raise error.ConfigError(_("%s.%s not a boolean ('%s')")
+                                    % (section, name, v))
+        return _booleans[v.lower()]
 
     def configlist(self, section, name, default=None, untrusted=False):
         """Return a list of comma/space separated strings"""
@@ -202,38 +177,20 @@
 
     def has_section(self, section, untrusted=False):
         '''tell whether section exists in config.'''
-        cdata = self._get_cdata(untrusted)
-        return cdata.has_section(section)
-
-    def _configitems(self, section, untrusted, abort):
-        items = {}
-        cdata = self._get_cdata(untrusted)
-        if cdata.has_section(section):
-            try:
-                items.update(dict(cdata.items(section)))
-            except ConfigParser.InterpolationError, inst:
-                msg = _("Error in configuration section [%s]:\n"
-                        "%s") % (section, inst)
-                if abort:
-                    raise util.Abort(msg)
-                self.warn(_("Ignored: %s\n") % msg)
-        return items
+        return section in self._get_cdata(untrusted)
 
     def configitems(self, section, untrusted=False):
-        items = self._configitems(section, untrusted=untrusted, abort=True)
+        items = self._get_cdata(untrusted).items(section)
         if self.debugflag and not untrusted:
-            uitems = self._configitems(section, untrusted=True, abort=False)
-            for k in util.sort(uitems):
-                if uitems[k] != items.get(k):
+            for k,v in self.ucdata.items(section):
+                if self.cdata.get(section, k) != v:
                     self.warn(_("Ignoring untrusted configuration option "
-                                "%s.%s = %s\n") % (section, k, uitems[k]))
-        return util.sort(items.items())
+                                "%s.%s = %s\n") % (section, k, v))
+        return items
 
     def walkconfig(self, untrusted=False):
         cdata = self._get_cdata(untrusted)
-        sections = cdata.sections()
-        sections.sort()
-        for section in sections:
+        for section in cdata.sections():
             for name, value in self.configitems(section, untrusted):
                 yield section, name, str(value).replace('\n', '\\n')
 
--- a/tests/test-hgrc.out	Thu Apr 23 15:40:10 2009 -0500
+++ b/tests/test-hgrc.out	Thu Apr 23 15:40:10 2009 -0500
@@ -1,16 +1,13 @@
-abort: Failed to parse .../t/.hg/hgrc
-File contains no section headers.
-file: .../t/.hg/hgrc, line: 1
-'invalid\n'
+hg: config error at .../t/.hg/hgrc:1: 'invalid'
 updating working directory
 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
 [paths]
 default = .../foo%%bar
-default = .../foo%bar
+default = .../foo%%bar
 bundle.mainreporoot=.../foobar
 defaults.backout=-d "0 0"
 defaults.commit=-d "0 0"
 defaults.debugrawcommit=-d "0 0"
 defaults.tag=-d "0 0"
-paths.default=.../foo%bar
+paths.default=.../foo%%bar
 ui.slash=True
--- a/tests/test-trusted.py	Thu Apr 23 15:40:10 2009 -0500
+++ b/tests/test-trusted.py	Thu Apr 23 15:40:10 2009 -0500
@@ -3,7 +3,7 @@
 # monkey-patching some functions in the util module
 
 import os
-from mercurial import ui, util
+from mercurial import ui, util, error
 
 hgrc = os.environ['HGRCPATH']
 f = open(hgrc)
@@ -85,7 +85,6 @@
 f = open('.hg/hgrc', 'w')
 f.write('[paths]\n')
 f.write('local = /another/path\n\n')
-f.write('interpolated = %(global)s%(local)s\n\n')
 f.close()
 
 #print '# Everything is run by user foo, group bar\n'
@@ -154,10 +153,8 @@
 u2.readconfig('.hg/hgrc')
 print 'trusted:'
 print u2.config('foobar', 'baz')
-print u2.config('paths', 'interpolated')
 print 'untrusted:'
 print u2.config('foobar', 'baz', untrusted=True)
-print u2.config('paths', 'interpolated', untrusted=True)
 
 print
 print "# error handling"
@@ -179,33 +176,15 @@
 print
 print "# parse error"
 f = open('.hg/hgrc', 'w')
-f.write('foo = bar')
-f.close()
-testui(user='abc', group='def', silent=True)
-assertraises(lambda: testui(debug=True, silent=True))
-
-print
-print "# interpolation error"
-f = open('.hg/hgrc', 'w')
-f.write('[foo]\n')
-f.write('bar = %(')
+f.write('foo')
 f.close()
-u = testui(debug=True, silent=True)
-print '# regular config:'
-print '  trusted',
-assertraises(lambda: u.config('foo', 'bar'))
-print 'untrusted',
-assertraises(lambda: u.config('foo', 'bar', untrusted=True))
 
-u = testui(user='abc', group='def', debug=True, silent=True)
-print '  trusted ',
-print u.config('foo', 'bar')
-print 'untrusted',
-assertraises(lambda: u.config('foo', 'bar', untrusted=True))
+try:
+    testui(user='abc', group='def', silent=True)
+except error.ConfigError, inst:
+    print inst
 
-print '# configitems:'
-print '  trusted ',
-print u.configitems('foo')
-print 'untrusted',
-assertraises(lambda: u.configitems('foo', untrusted=True))
-
+try:
+    testui(debug=True, silent=True)
+except error.ConfigError, inst:
+    print inst
--- a/tests/test-trusted.py.out	Thu Apr 23 15:40:10 2009 -0500
+++ b/tests/test-trusted.py.out	Thu Apr 23 15:40:10 2009 -0500
@@ -1,21 +1,17 @@
 # same user, same group
 trusted
     global = /some/path
-    interpolated = /some/path/another/path
     local = /another/path
 untrusted
 . . global = /some/path
-. . interpolated = /some/path/another/path
 . . local = /another/path
 
 # same user, different group
 trusted
     global = /some/path
-    interpolated = /some/path/another/path
     local = /another/path
 untrusted
 . . global = /some/path
-. . interpolated = /some/path/another/path
 . . local = /another/path
 
 # different user, same group
@@ -24,17 +20,14 @@
     global = /some/path
 untrusted
 . . global = /some/path
-. . interpolated = /some/path/another/path
 . . local = /another/path
 
 # different user, same group, but we trust the group
 trusted
     global = /some/path
-    interpolated = /some/path/another/path
     local = /another/path
 untrusted
 . . global = /some/path
-. . interpolated = /some/path/another/path
 . . local = /another/path
 
 # different user, different group
@@ -43,70 +36,57 @@
     global = /some/path
 untrusted
 . . global = /some/path
-. . interpolated = /some/path/another/path
 . . local = /another/path
 
 # different user, different group, but we trust the user
 trusted
     global = /some/path
-    interpolated = /some/path/another/path
     local = /another/path
 untrusted
 . . global = /some/path
-. . interpolated = /some/path/another/path
 . . local = /another/path
 
 # different user, different group, but we trust the group
 trusted
     global = /some/path
-    interpolated = /some/path/another/path
     local = /another/path
 untrusted
 . . global = /some/path
-. . interpolated = /some/path/another/path
 . . local = /another/path
 
 # different user, different group, but we trust the user and the group
 trusted
     global = /some/path
-    interpolated = /some/path/another/path
     local = /another/path
 untrusted
 . . global = /some/path
-. . interpolated = /some/path/another/path
 . . local = /another/path
 
 # we trust all users
 # different user, different group
 trusted
     global = /some/path
-    interpolated = /some/path/another/path
     local = /another/path
 untrusted
 . . global = /some/path
-. . interpolated = /some/path/another/path
 . . local = /another/path
 
 # we trust all groups
 # different user, different group
 trusted
     global = /some/path
-    interpolated = /some/path/another/path
     local = /another/path
 untrusted
 . . global = /some/path
-. . interpolated = /some/path/another/path
 . . local = /another/path
 
 # we trust all users and groups
 # different user, different group
 trusted
     global = /some/path
-    interpolated = /some/path/another/path
     local = /another/path
 untrusted
 . . global = /some/path
-. . interpolated = /some/path/another/path
 . . local = /another/path
 
 # we don't get confused by users and groups with the same name
@@ -116,29 +96,24 @@
     global = /some/path
 untrusted
 . . global = /some/path
-. . interpolated = /some/path/another/path
 . . local = /another/path
 
 # list of user names
 # different user, different group, but we trust the user
 trusted
     global = /some/path
-    interpolated = /some/path/another/path
     local = /another/path
 untrusted
 . . global = /some/path
-. . interpolated = /some/path/another/path
 . . local = /another/path
 
 # list of group names
 # different user, different group, but we trust the group
 trusted
     global = /some/path
-    interpolated = /some/path/another/path
     local = /another/path
 untrusted
 . . global = /some/path
-. . interpolated = /some/path/another/path
 . . local = /another/path
 
 # Can't figure out the name of the user running this process
@@ -148,20 +123,16 @@
     global = /some/path
 untrusted
 . . global = /some/path
-. . interpolated = /some/path/another/path
 . . local = /another/path
 
 # prints debug warnings
 # different user, different group
 Not trusting file .hg/hgrc from untrusted user abc, group def
 trusted
-Ignoring untrusted configuration option paths.interpolated = /some/path/another/path
 Ignoring untrusted configuration option paths.local = /another/path
     global = /some/path
 untrusted
 . . global = /some/path
-.Ignoring untrusted configuration option paths.interpolated = /some/path/another/path
- . interpolated = /some/path/another/path
 .Ignoring untrusted configuration option paths.local = /another/path
  . local = /another/path
 
@@ -173,10 +144,8 @@
 trusted:
 Ignoring untrusted configuration option foobar.baz = quux
 None
-/some/path/another/path
 untrusted:
 quux
-/some/path/another/path
 
 # error handling
 # file doesn't exist
@@ -186,26 +155,6 @@
 # parse error
 # different user, different group
 Not trusting file .hg/hgrc from untrusted user abc, group def
-Ignored: Failed to parse .hg/hgrc
-File contains no section headers.
-file: .hg/hgrc, line: 1
-'foo = bar'
-# same user, same group
-raised Abort
-
-# interpolation error
+Ignored: config error at .hg/hgrc:1: 'foo'
 # same user, same group
-# regular config:
-  trusted raised Abort
-untrusted raised Abort
-# different user, different group
-Not trusting file .hg/hgrc from untrusted user abc, group def
-  trusted Ignored: Error in configuration section [foo] parameter 'bar':
-bad interpolation variable reference '%('
- None
-untrusted raised Abort
-# configitems:
-  trusted Ignored: Error in configuration section [foo]:
-bad interpolation variable reference '%('
- []
-untrusted raised Abort
+config error at .hg/hgrc:1: 'foo'
--- a/tests/test-ui-config	Thu Apr 23 15:40:10 2009 -0500
+++ b/tests/test-ui-config	Thu Apr 23 15:40:10 2009 -0500
@@ -1,7 +1,6 @@
 #!/usr/bin/env python
 
-import ConfigParser
-from mercurial import ui, util, dispatch
+from mercurial import ui, util, dispatch, error
 
 testui = ui.ui()
 parsed = dispatch._parseconfig(testui, [
@@ -12,19 +11,10 @@
     'lists.list2=foo bar baz',
     'lists.list3=alice, bob',
     'lists.list4=foo bar baz alice, bob',
-    'interpolation.value1=hallo',
-    'interpolation.value2=%(value1)s world',
-    'interpolation.value3=%(novalue)s',
-    'interpolation.value4=%(bad)1',
-    'interpolation.value5=%bad2',
 ])
 
 print repr(testui.configitems('values'))
 print repr(testui.configitems('lists'))
-try:
-    print repr(testui.configitems('interpolation'))
-except util.Abort, inst:
-    print inst
 print "---"
 print repr(testui.config('values', 'string'))
 print repr(testui.config('values', 'bool1'))
@@ -33,7 +23,7 @@
 print "---"
 try:
     print repr(testui.configbool('values', 'string'))
-except util.Abort, inst:
+except error.ConfigError, inst:
     print inst
 print repr(testui.configbool('values', 'bool1'))
 print repr(testui.configbool('values', 'bool2'))
@@ -54,37 +44,12 @@
 print repr(testui.configlist('lists', 'unknown', 'foo, bar'))
 print repr(testui.configlist('lists', 'unknown', ['foo bar']))
 print repr(testui.configlist('lists', 'unknown', ['foo', 'bar']))
-print "---"
-print repr(testui.config('interpolation', 'value1'))
-print repr(testui.config('interpolation', 'value2'))
-try:
-    print repr(testui.config('interpolation', 'value3'))
-except util.Abort, inst:
-    print inst
-try:
-    print repr(testui.config('interpolation', 'value4'))
-except util.Abort, inst:
-    print inst
-try:
-    print repr(testui.config('interpolation', 'value5'))
-except util.Abort, inst:
-    print inst
-print "---"
 
-cp = util.configparser()
-cp.add_section('foo')
-cp.set('foo', 'bar', 'baz')
-try:
-    # should fail - keys are case-sensitive
-    cp.get('foo', 'Bar')
-except ConfigParser.NoOptionError, inst:
-    print inst
+print repr(testui.config('values', 'String'))
 
 def function():
     pass
 
-cp.add_section('hook')
 # values that aren't strings should work
-cp.set('hook', 'commit', function)
-f = cp.get('hook', 'commit')
-print "f %s= function" % (f == function and '=' or '!')
+testui.setconfig('hook', 'commit', function)
+print function == testui.config('hook', 'commit')
--- a/tests/test-ui-config.out	Thu Apr 23 15:40:10 2009 -0500
+++ b/tests/test-ui-config.out	Thu Apr 23 15:40:10 2009 -0500
@@ -1,15 +1,12 @@
-[('bool1', 'true'), ('bool2', 'false'), ('string', 'string value')]
+[('string', 'string value'), ('bool1', 'true'), ('bool2', 'false')]
 [('list1', 'foo'), ('list2', 'foo bar baz'), ('list3', 'alice, bob'), ('list4', 'foo bar baz alice, bob')]
-Error in configuration section [interpolation]:
-'%' must be followed by '%' or '(', found: '%bad2'
 ---
 'string value'
 'true'
 'false'
 None
 ---
-Error in configuration section [values] parameter 'string':
-Not a boolean: string value
+values.string not a boolean ('string value')
 True
 False
 False
@@ -29,20 +26,5 @@
 ['foo', 'bar']
 ['foo bar']
 ['foo', 'bar']
----
-'hallo'
-'hallo world'
-Error in configuration section [interpolation] parameter 'value3':
-Bad value substitution:
-	section: [interpolation]
-	option : value3
-	key    : novalue
-	rawval : 
-
-Error in configuration section [interpolation] parameter 'value4':
-bad interpolation variable reference '%(bad)1'
-Error in configuration section [interpolation] parameter 'value5':
-'%' must be followed by '%' or '(', found: '%bad2'
----
-No option 'Bar' in section: 'foo'
-f == function
+None
+True