peerdb/tripe-newpeers.in (ConfigSection.items): Report `name'.
[tripe] / peerdb / tripe-newpeers.in
index 37c0a34..00361d4 100644 (file)
@@ -156,6 +156,12 @@ class InheritanceCycleError (Exception):
     return "Found a cycle %s looking up key `%s'" % \
         (_fmt_path(me.path), me.key)
 
     return "Found a cycle %s looking up key `%s'" % \
         (_fmt_path(me.path), me.key)
 
+class MissingSectionException (Exception):
+  def __init__(me, sec):
+    me.key = key
+  def __str__(me):
+    return "Section `%s' not found" % (me.sec)
+
 class MissingKeyException (Exception):
   def __init__(me, sec, key):
     me.sec = sec
 class MissingKeyException (Exception):
   def __init__(me, sec, key):
     me.sec = sec
@@ -163,6 +169,167 @@ class MissingKeyException (Exception):
   def __str__(me):
     return "Key `%s' not found in section `%s'" % (me.key, me.sec)
 
   def __str__(me):
     return "Key `%s' not found in section `%s'" % (me.key, me.sec)
 
+class ConfigSection (object):
+  """
+  A section in a configuration parser.
+
+  This is where a lot of the nitty-gritty stuff actually happens.  The
+  `MyConfigParser' knows a lot about the internals of this class, which saves
+  on building a complicated interface.
+  """
+
+  def __init__(me, name, cp):
+    """Initialize a new, empty section with a given NAME and parent CP."""
+
+    ## The cache maps item keys to entries, which consist of a pair of
+    ## objects.  There are four possible states for a cache entry:
+    ##
+    ##   * missing -- there is no entry at all with this key, so we must
+    ##     search for it;
+    ##
+    ##   * None, None -- we are actively trying to resolve this key, so if we
+    ##     encounter this state, we have found a cycle in the inheritance
+    ##     graph;
+    ##
+    ##   * None, [] -- we know that this key isn't reachable through any of
+    ##     our parents;
+    ##
+    ##   * VALUE, PATH -- we know that the key resolves to VALUE, along the
+    ##     PATH from us (exclusive) to the defining parent (inclusive).
+    me.name = name
+    me._itemmap = dict()
+    me._cache = dict()
+    me._cp = cp
+
+  def _expand(me, string, resolvep):
+    """
+    Expands $(...) and (optionally) $[...] placeholders in STRING.
+
+    RESOLVEP is a boolean switch: do we bother to tax the resolver or not?
+    This is turned off by MyConfigParser's resolve() method while it's
+    collecting hostnames to be resolved.
+    """
+    string = RX_REF.sub \
+             (lambda m: me.get(m.group(1), resolvep), string)
+    if resolvep:
+      string = RX_RESOLVE.sub(lambda m: me._cp._resolver.lookup(m.group(1)),
+                              string)
+    return string
+
+  def _parents(me):
+    """Yield this section's parents."""
+    try: names = me._itemmap['@inherit']
+    except KeyError: return
+    for name in names.replace(',', ' ').split():
+      yield me._cp.section(name)
+
+  def _get(me, key, path = None):
+    """
+    Low-level option-fetching method.
+
+    Fetch the value for the named KEY in this section, or maybe (recursively)
+    a section which it inherits from.
+
+    Returns a pair VALUE, PATH.  The value is not expanded; nor do we check
+    for the special `name' key.  The caller is expected to do these things.
+    Returns None if no value could be found.
+    """
+
+    ## If we weren't given a path, then we'd better make one.
+    if path is None: path = []
+
+    ## Extend the path to cover us, but remember to remove us again when
+    ## we've finished.  If we need to pass the current path back upwards,
+    ## then remember to take a copy.
+    path.append(me.name)
+    try:
+
+      ## If we've been this way before on another pass through then return the
+      ## value we found then.  If we're still thinking about it then we've
+      ## found a cycle.
+      try: v, p = me._cache[key]
+      except KeyError: pass
+      else:
+        if p is None: raise InheritanceCycleError(key, path[:])
+        else: return v, path + p
+
+      ## See whether the answer is ready waiting for us.
+      try: v = me._itemmap[key]
+      except KeyError: pass
+      else:
+        p = path[:]
+        me._cache[key] = v, []
+        return v, p
+
+      ## Initially we have no idea.
+      value = None
+      winner = []
+
+      ## Go through our parents and ask them what they think.
+      me._cache[key] = None, None
+      for p in me._parents():
+
+        ## See whether we get an answer.  If not, keep on going.
+        v, pp = p._get(key, path)
+        if v is None: continue
+
+        ## If we got an answer, check that it matches any previous ones.
+        if value is None:
+          value = v
+          winner = pp
+        elif value != v:
+          raise AmbiguousOptionError(key, winner, value, pp, v)
+
+      ## That's the best we could manage.
+      me._cache[key] = value, winner[len(path):]
+      return value, winner
+
+    finally:
+      ## Remove us from the path again.
+      path.pop()
+
+  def get(me, key, resolvep = True):
+    """
+    Retrieve the value of KEY from this section.
+    """
+
+    ## Special handling for the `name' key.
+    if key == 'name':
+      value = me._itemmap.get('name', me.name)
+    elif key == '@inherits':
+      try: return me._itemmap['@inherits']
+      except KeyError: raise MissingKeyException(me.name, key)
+    else:
+      value, _ = me._get(key)
+      if value is None:
+        raise MissingKeyException(me.name, key)
+
+    ## Expand the value and return it.
+    return me._expand(value, resolvep)
+
+  def items(me, resolvep = True):
+    """
+    Yield a list of item names in the section.
+    """
+
+    ## Initialize for a depth-first walk of the inheritance graph.
+    seen = { 'name': True }
+    visiting = { me.name: True }
+    stack = [me]
+
+    ## Visit nodes, collecting their keys.  Don't believe the values:
+    ## resolving inheritance is too hard to do like this.
+    while stack:
+      sec = stack.pop()
+      for p in sec._parents():
+        if p.name not in visiting:
+          stack.append(p); visiting[p.name] = True
+
+      for key in sec._itemmap.iterkeys(): seen[key] = None
+
+    ## And we're done.
+    return seen.iterkeys()
+
 class MyConfigParser (object):
   """
   A more advanced configuration parser.
 class MyConfigParser (object):
   """
   A more advanced configuration parser.
@@ -188,7 +355,10 @@ class MyConfigParser (object):
     2. Call resolve() to collect the hostnames which need to be resolved and
        actually do the name resolution.
 
     2. Call resolve() to collect the hostnames which need to be resolved and
        actually do the name resolution.
 
-    3. Call get(SECTION, ITEM) to collect the results, or items(SECTION) to
+    3. Call sections() to get a list of the configuration sections, or
+       section(NAME) to find a named section.
+
+    4. Call get(ITEM) on a section to collect the results, or items() to
        iterate over them.
   """
 
        iterate over them.
   """
 
@@ -219,7 +389,7 @@ class MyConfigParser (object):
     ## Commit a key's value when we've determined that there are no further
     ## continuation lines.
     def flush():
     ## Commit a key's value when we've determined that there are no further
     ## continuation lines.
     def flush():
-      if key is not None: sect[key] = val.getvalue()
+      if key is not None: sect._itemmap[key] = val.getvalue()
 
     ## Work through all of the input lines.
     for line in f:
 
     ## Work through all of the input lines.
     for line in f:
@@ -239,7 +409,7 @@ class MyConfigParser (object):
         flush()
         name = m[0].group(1)
         try: sect = me._sectmap[name]
         flush()
         name = m[0].group(1)
         try: sect = me._sectmap[name]
-        except KeyError: sect = me._sectmap[name] = dict()
+        except KeyError: sect = me._sectmap[name] = ConfigSection(name, me)
         key = None
 
       elif match(RX_ASSGN):
         key = None
 
       elif match(RX_ASSGN):
@@ -267,9 +437,14 @@ class MyConfigParser (object):
     ## Don't forget to commit any final value material.
     flush()
 
     ## Don't forget to commit any final value material.
     flush()
 
+  def section(me, name):
+    """Return a ConfigSection with the given NAME."""
+    try: return me._sectmap[name]
+    except KeyError: raise MissingSectionException(name)
+
   def sections(me):
   def sections(me):
-    """Yield the known section names."""
-    return me._sectmap.iterkeys()
+    """Yield the known sections."""
+    return me._sectmap.itervalues()
 
   def resolve(me):
     """
 
   def resolve(me):
     """
@@ -278,152 +453,13 @@ class MyConfigParser (object):
     Until you call this, attempts to fetch configuration items which need to
     resolve hostnames will fail!
     """
     Until you call this, attempts to fetch configuration items which need to
     resolve hostnames will fail!
     """
-    for sec in me._sectmap.iterkeys():
-      for key, value in me.items(sec, resolvep = False):
+    for sec in me.sections():
+      for key in sec.items():
+        value = sec.get(key, resolvep = False)
         for match in RX_RESOLVE.finditer(value):
           me._resolver.prepare(match.group(1))
     me._resolver.run()
 
         for match in RX_RESOLVE.finditer(value):
           me._resolver.prepare(match.group(1))
     me._resolver.run()
 
-  def _expand(me, sec, string, resolvep):
-    """
-    Expands $(...) and (optionally) $[...] placeholders in STRING.
-
-    The SEC is the configuration section from which to satisfy $(...)
-    requests.  RESOLVEP is a boolean switch: do we bother to tax the resolver
-    or not?  This is turned off by the resolve() method while it's collecting
-    hostnames to be resolved.
-    """
-    string = RX_REF.sub \
-             (lambda m: me.get(sec, m.group(1), resolvep), string)
-    if resolvep:
-      string = RX_RESOLVE.sub(lambda m: me._resolver.lookup(m.group(1)),
-                              string)
-    return string
-
-  def has_option(me, sec, key):
-    """
-    Decide whether section SEC has a configuration key KEY.
-
-    This version of the method properly handles the @inherit key.
-    """
-    return key == 'name' or me._get(sec, key)[0] is not None
-
-  def _get(me, sec, key, map = None, path = None):
-    """
-    Low-level option-fetching method.
-
-    Fetch the value for the named KEY from section SEC, or maybe
-    (recursively) a section which SEC inherits from.
-
-    Returns a pair VALUE, PATH.  The value is not expanded; nor do we check
-    for the special `name' key.  The caller is expected to do these things.
-    Returns None if no value could be found.
-    """
-
-    ## If we weren't given a memoization map or path, then we'd better make
-    ## one.
-    if map is None: map = {}
-    if path is None: path = []
-
-    ## Extend the path to cover the lookup section, but remember to remove us
-    ## again when we've finished.  If we need to pass the current path back
-    ## upwards, then remember to take a copy.
-    path.append(sec)
-    try:
-
-      ## If we've been this way before on another pass through then return
-      ## the value we found then.  If we're still thinking about it then
-      ## we've found a cycle.
-      try: threadp, value = map[sec]
-      except KeyError: pass
-      else:
-        if threadp: raise InheritanceCycleError(key, path[:])
-
-      ## See whether the answer is ready waiting for us.
-      try: v = me._sectmap[sec][key]
-      except KeyError: pass
-      else: return v, path[:]
-
-      ## No, apparently, not.  Find out our list of parents.
-      try:
-        parents = me._sectmap[sec]['@inherit'].replace(',', ' ').split()
-      except KeyError:
-        parents = []
-
-      ## Initially we have no idea.
-      value = None
-      winner = None
-
-      ## Go through our parents and ask them what they think.
-      map[sec] = True, None
-      for p in parents:
-
-        ## See whether we get an answer.  If not, keep on going.
-        v, pp = me._get(p, key, map, path)
-        if v is None: continue
-
-        ## If we got an answer, check that it matches any previous ones.
-        if value is None:
-          value = v
-          winner = pp
-        elif value != v:
-          raise AmbiguousOptionError(key, winner, value, pp, v)
-
-      ## That's the best we could manage.
-      map[sec] = False, value
-      return value, winner
-
-    finally:
-      ## Remove us from the path again.
-      path.pop()
-
-  def get(me, sec, key, resolvep = True):
-    """
-    Retrieve the value of KEY from section SEC.
-    """
-
-    ## Special handling for the `name' key.
-    if key == 'name':
-      value = me._sectmap[sec].get('name', sec)
-    else:
-      value, _ = me._get(sec, key)
-      if value is None:
-        raise MissingKeyException(sec, key)
-
-    ## Expand the value and return it.
-    return me._expand(sec, value, resolvep)
-
-  def items(me, sec, resolvep = True):
-    """
-    Return a list of (NAME, VALUE) items in section SEC.
-
-    This extends the default method by handling the inheritance chain.
-    """
-
-    ## Initialize for a depth-first walk of the inheritance graph.
-    d = {}
-    visited = {}
-    basesec = sec
-    stack = [sec]
-
-    ## Visit nodes, collecting their keys.  Don't believe the values:
-    ## resolving inheritance is too hard to do like this.
-    while stack:
-      sec = stack.pop()
-      if sec in visited: continue
-      visited[sec] = True
-
-      for key, value in me._sectmap[sec].iteritems():
-        if key == '@inherit': stack += value.replace(',', ' ').split()
-        else: d[key] = None
-
-    ## Now collect the values for the known keys, one by one.
-    items = []
-    for key in d: items.append((key, me.get(basesec, key, resolvep)))
-
-    ## And we're done.
-    return items
-
 ###--------------------------------------------------------------------------
 ### Command-line handling.
 
 ###--------------------------------------------------------------------------
 ### Command-line handling.
 
@@ -490,22 +526,24 @@ def output(conf, cdb):
   This is where the special `user' and `auto' database entries get set.
   """
   auto = []
   This is where the special `user' and `auto' database entries get set.
   """
   auto = []
-  for sec in sorted(conf.sections()):
-    if sec.startswith('@'):
+  for sec in sorted(conf.sections(), key = lambda sec: sec.name):
+    if sec.name.startswith('@'):
       continue
       continue
-    elif sec.startswith('$'):
-      label = sec
+    elif sec.name.startswith('$'):
+      label = sec.name
     else:
     else:
-      label = 'P%s' % sec
-      if conf.has_option(sec, 'auto') and \
-         conf.get(sec, 'auto') in ('y', 'yes', 't', 'true', '1', 'on'):
-        auto.append(sec)
-      if conf.has_option(sec, 'user'):
-        cdb.add('U%s' % conf.get(sec, 'user'), sec)
-    url = M.URLEncode(laxp = True, semip = True)
-    for key, value in sorted(conf.items(sec), key = lambda (k, v): k):
+      label = 'P%s' % sec.name
+      try: a = sec.get('auto')
+      except MissingKeyException: pass
+      else:
+        if a in ('y', 'yes', 't', 'true', '1', 'on'): auto.append(sec.name)
+      try: u = sec.get('user')
+      except MissingKeyException: pass
+      else: cdb.add('U%s' % u)
+    url = M.URLEncode(semip = True)
+    for key in sorted(sec.items()):
       if not key.startswith('@'):
       if not key.startswith('@'):
-        url.encode(key, ' '.join(M.split(value)[0]))
+        url.encode(key, sec.get(key))
     cdb.add(label, url.result)
   cdb.add('%AUTO', ' '.join(auto))
   cdb.finish()
     cdb.add(label, url.result)
   cdb.add('%AUTO', ' '.join(auto))
   cdb.finish()