# HG changeset patch # User Matt Mackall # Date 1240519210 18000 # Node ID fca54469480e0f496556572579d3d2c5d78b2ec9 # Parent 507c49e297e1d43af28871b47b1ed7279e7fb942 ui: introduce new config parser diff -r 507c49e297e1 -r fca54469480e mercurial/config.py --- /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())) diff -r 507c49e297e1 -r fca54469480e mercurial/dispatch.py --- 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 diff -r 507c49e297e1 -r fca54469480e mercurial/error.py --- 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 diff -r 507c49e297e1 -r fca54469480e mercurial/ui.py --- 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') diff -r 507c49e297e1 -r fca54469480e tests/test-hgrc.out --- 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 diff -r 507c49e297e1 -r fca54469480e tests/test-trusted.py --- 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 diff -r 507c49e297e1 -r fca54469480e tests/test-trusted.py.out --- 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' diff -r 507c49e297e1 -r fca54469480e tests/test-ui-config --- 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') diff -r 507c49e297e1 -r fca54469480e tests/test-ui-config.out --- 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