[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[minion-cvs] Resolve all DOCDOCs and most XXXX006s.



Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv16632/lib/mixminion

Modified Files:
	BuildMessage.py ClientDirectory.py ClientMain.py Common.py 
	Config.py MMTPClient.py Main.py Packet.py ServerInfo.py 
	test.py 
Log Message:
Resolve all DOCDOCs and most XXXX006s.

Additionally, tweak the list-servers interface a bit.


Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.62
retrieving revision 1.63
diff -u -d -r1.62 -r1.63
--- BuildMessage.py	20 Nov 2003 08:51:27 -0000	1.62
+++ BuildMessage.py	24 Nov 2003 19:59:03 -0000	1.63
@@ -83,7 +83,7 @@
     return fragments
 
 def buildRandomPayload(paddingPRNG=None):
-    """DOCDOC"""
+    """Return a new random payload, suitable for use in a DROP packet."""
     if not paddingPRNG:
         paddingPRNG = Crypto.getCommonPRNG()
     return paddingPRNG.getBytes(PAYLOAD_LEN)
@@ -110,7 +110,7 @@
         raise MixError("Second leg of path is empty")
 
     suppressTag = 0
-    #XXXX006 refactor _TYPES_WITHOUT_TAGS
+    #XXXX refactor _TYPES_WITHOUT_TAGS
     if exitType == DROP_TYPE or mixminion.Packet._TYPES_WITHOUT_TAGS.get(exitType):
         suppressTag = 1
 
@@ -333,14 +333,16 @@
     
 #----------------------------------------------------------------------
 # MESSAGE DECODING
-def decodePayload(payload, tag, key=None, userKeys=None):
+def decodePayload(payload, tag, key=None, userKeys=()):
     """Given a 28K payload and a 20-byte decoding tag, attempt to decode the
        original message.  Returns either a SingletonPayload instance, a
        FragmentPayload instance, or None.
 
            key: an RSA key to decode encrypted forward messages, or None
-           userKeys: a map from identity names to keys for reply blocks,
-                or None. DOCDOC : prefer list of (name,key)
+           userKeys: a sequence of (name,key) tuples maping identity names to 
+                SURB keys. For backward compatibility, 'userKeys' may also be
+                None (no SURBs known), a dict (from name to key), or a single
+                key (implied identity is "").
 
        If we can successfully decrypt the payload, we return it.  If we
        might be able to decrypt the payload given more/different keys,

Index: ClientDirectory.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientDirectory.py,v
retrieving revision 1.17
retrieving revision 1.18
diff -u -d -r1.17 -r1.18
--- ClientDirectory.py	24 Nov 2003 02:53:39 -0000	1.17
+++ ClientDirectory.py	24 Nov 2003 19:59:03 -0000	1.18
@@ -5,8 +5,8 @@
    dealing with mixminion directories.  This includes:
      - downloading and caching directories
      - path generation
-   DOCDOC
-     """
+     - address parsing.
+"""
 
 __all__ = [ 'ClientDirectory', 'parsePath', 'parseAddress' ]
 
@@ -64,9 +64,9 @@
     # goodServerNicknames: A map from lowercased nicknames of recommended
     #    servers to 1.
     ## Layout:
-    # DIR/cache: A cPickled tuple of ("ClientKeystore-0.2",
-    #         lastModified, lastDownload, clientVersions, serverlist,
-    #         fullServerList, digestMap) DOCDOC is this correct?
+    # DIR/cache: A cPickled tuple of ("ClientKeystore-0.3",
+    #         lastModified, lastDownload, clientVersions, serverList,
+    #         fullServerList, digestMap)
     # DIR/dir.gz *or* DIR/dir: A (possibly gzipped) directory file.
     # DIR/imported/: A directory of server descriptors.
     MAGIC = "ClientKeystore-0.3"
@@ -104,7 +104,8 @@
 
     def downloadDirectory(self, timeout=None):
         """Download a new directory from the network, validate it, and
-           rescan its servers. DOCDOC timeout"""
+           rescan its servers.  If the operation doesn't complete within
+           timeout seconds, raise an error."""
         # Start downloading the directory.
         url = MIXMINION_DIRECTORY_URL
         LOG.info("Downloading directory from %s", url)
@@ -279,7 +280,8 @@
         writePickled(os.path.join(self.dir, "cache"), data)
 
     def _installAsKeyIDResolver(self):
-        """DOCDOC"""
+        """Use this ClientDirectory to identify servers in calls to 
+           ServerInfo.displayServer."""
         mixminion.ServerInfo._keyIDToNicknameFn = self.getNicknameByKeyID
 
     def importFromFile(self, filename):
@@ -743,12 +745,11 @@
         if endAt is None: endAt = startAt+self.DEFAULT_REQUIRED_LIFETIME
 
         p = pathSpec.path1+pathSpec.path2
+        assert p
         # Make sure all elements are valid.
         for e in p:
             e.validate(self, startAt, endAt)
 
-        #XXXX006 make sure p can never be empty!
-
         # If there is a 1st element, make sure we can route to it.
         fixed = p[0].getFixedServer(self, startAt, endAt)
         if fixed and not fixed.canStartAt():
@@ -875,7 +876,8 @@
     
     return result
 
-def formatFeatureMap(features, featureMap, showTime=0, cascade=0, sep=" "):
+def formatFeatureMap(features, featureMap, showTime=0, cascade=0, sep=" ",
+                     just=0):
     """Given a list of features (by name; see Config.resolveFeatureName) and
        a featureMap as returned by ClientDirectory.getFeatureMap or
        compressFeatureMap, formats the map for display to an end users.
@@ -895,25 +897,42 @@
 
        'sep' is used to concatenate feauture values when putting them on
        the same line.
+
+       If 'just' is true, we left-justify features in columns.
        """
     nicknames = [ (nn.lower(), nn) for nn in featureMap.keys() ]
     nicknames.sort()
     lines = []
     if not nicknames: return lines
-    maxnicklen = max([len(nn) for nn in nicknames])
-    nnformat = "%-"+str(maxnicklen)+"s"
+
+    if just:
+        maxnicklen = max([len(nn) for nn in featureMap.keys()])
+        nnformat = "%-"+str(maxnicklen)+"s"
+        maxFeatureLength = {}
+        for f in features: maxFeatureLength[f] = 0
+        for byTime in featureMap.values():
+            for fMap in byTime.values():
+                for k, v in fMap.items():
+                    if maxFeatureLength[k] < len(v):
+                        maxFeatureLength[k] = len(v)
+        formatEntries = [ "%-"+str(maxFeatureLength[f])+"s" for 
+                          f in features ]
+        format = sep.join(formatEntries)
+    else:
+        nnformat = "%s"
+        format = sep.join(["%s"]*len(features))
+
     for _, nickname in nicknames:
         d = featureMap[nickname]
         if not d: continue
         items = d.items()
         items.sort()
         if cascade: lines.append("%s:"%nickname)
-        justified_nickname = nnformat%nickname
         for (va,vu),fmap in items:
             ftime = "%s to %s"%(formatDate(va),formatDate(vu))
+            fvals = tuple([fmap[f] for f in features])
             if cascade==1:
-                lines.append("  [%s] %s"%(ftime,
-                        sep.join([fmap[f] for f in features])))
+                lines.append("  [%s] %s"%(ftime, format%fvals))
             elif cascade==2:
                 if showTime:
                     lines.append("  [%s]"%ftime)
@@ -921,11 +940,10 @@
                     v = fmap[f]
                     lines.append("    %s:%s"%(f,v))
             elif showTime:
-                lines.append("%s:%s:%s" %(justified_nickname,ftime,
-                   sep.join([fmap[f] for f in features])))
+                lines.append("%s:%s:%s" %(nnformat%nickname,ftime,
+                                          format%fvals))
             else:
-                lines.append("%s:%s" %(justified_nickname,
-                   sep.join([fmap[f] for f in features])))
+                lines.append("%s:%s" %(nnformat%nickname,format%fvals))
     return lines
 
 #----------------------------------------------------------------------
@@ -1377,9 +1395,8 @@
     if not path:
         path = "*%d"%(nHops or defaultNHops or 6)
     # Break path into a list of entries of the form:
-    #        Nickname
+    #        string
     #     or "<swap>"
-    #     or "?"
     p = []
     while path:
         if path[0] == "'":
@@ -1412,6 +1429,8 @@
             path = path[1:]
             p.append("<swap>")
 
+    # Convert each parsed entry into a PathElement, or the string
+    # '*', or the string '<swap>'.
     pathEntries = []
     for ent in p:
         if re.match(r'\*(\d+)', ent):
@@ -1429,13 +1448,15 @@
 
     # If there's a variable-length wildcard...
     if "*" in pathEntries:
-        # Find out how many hops we should have.
+        # Find out where it is...
         starPos = pathEntries.index("*")
         if "*" in pathEntries[starPos+1:]:
             raise UIError("Only one '*' is permitted in a single path")
+        # Figure out how many hops we expect to have...
         approxHops = reduce(operator.add,
                             [ ent.getAvgLength() for ent in pathEntries
                               if ent not in ("*", "<swap>") ], 0)
+        # Replace the '*' with the number of additional hops we want.
         myNHops = nHops or defaultNHops or 6
         extraHops = max(myNHops-approxHops, 0)
         pathEntries[starPos:starPos+1] =[RandomServersPathElement(n=extraHops)]
@@ -1447,24 +1468,33 @@
     # Figure out how long the first leg should be.
     lateSplit = 0
     if "<swap>" in pathEntries:
-        # Calculate colon position
+        # We got a colon...
         if halfPath:
+            # ...in a reply or SURB. That's an error.
             raise UIError("Can't specify swap point with replies")
+        # Divide the path at the '<swap>'.
         colonPos = pathEntries.index("<swap>")
         if "<swap>" in pathEntries[colonPos+1:]:
             raise UIError("Only one ':' is permitted in a single path")
         firstLegLen = colonPos
         del pathEntries[colonPos]
     elif isReply:
+        # A reply message is all first leg.
         firstLegLen = len(pathEntries)
     elif isSURB:
+        # A SURB is all second-leg.
         firstLegLen = 0
     else:
+        # We have no explicit swap point, but we have a foward message.  Thus,
+        # we set 'lateSplit' so that we'll know to divide the path into two
+        # legs later on.
         firstLegLen = 0
         lateSplit = 1
 
-    # This is a kludge to convert paths of the form ~N to ?,~(N-1) when we've
-    # got a full path.
+    # This is a kludge to convert paths of the form ~N to ?,~(N-1), when 
+    # we're generating a two-legged path.  Otherwise, there is a possibility
+    # that ~N could expand into only a single server, thus leaving one leg
+    # empty.
     if (len(pathEntries) == 1
         and not halfPath
         and isinstance(pathEntries[0], RandomServersPathElement)
@@ -1472,13 +1502,11 @@
         n_minus_1 = max(pathEntries[0].approx-1,0)
         pathEntries = [ RandomServersPathElement(n=1),
                         RandomServersPathElement(approx=n_minus_1) ]
-        lateSplit = 1 # XXXX Is this redundant?
+        assert lateSplit
         
-    # Split the path into 2 legs.
     path1, path2 = pathEntries[:firstLegLen], pathEntries[firstLegLen:]
 
-    # XXXX006 when checking lengths, if the specifier is something like ~5,
-    # XXXX006 we should convert it to something more like *2,~3.
+    # Die if the path is too short, or if either leg is empty in a full path.
     if not lateSplit and not halfPath:
         if len(path1)+len(path2) < 2:
             raise UIError("The path must have at least 2 hops")

Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.131
retrieving revision 1.132
diff -u -d -r1.131 -r1.132
--- ClientMain.py	20 Nov 2003 08:51:27 -0000	1.131
+++ ClientMain.py	24 Nov 2003 19:59:04 -0000	1.132
@@ -21,10 +21,11 @@
 import mixminion.Crypto
 import mixminion.Filestore
 import mixminion.MMTPClient
+import mixminion.ServerInfo
 
 from mixminion.Common import LOG, Lockfile, LockfileLocked, MixError, \
      MixFatalError, MixProtocolBadAuth, MixProtocolError, UIError, \
-     UsageError, createPrivateDir, isPrintingAscii, isSMTPMailbox, readFile, \
+     UsageError, createPrivateDir, englishSequence, isPrintingAscii, isSMTPMailbox, readFile, \
      stringContains, succeedingMidnight, writeFile, previousMidnight, floorDiv
 from mixminion.Packet import encodeMailHeaders, ParseError, parseMBOXInfo, \
      parseReplyBlocks, parseSMTPInfo, parseTextEncodedMessages, \
@@ -61,32 +62,42 @@
        is limited to a single SURB decryption key.  In the future, we may
        include more SURB keys, as well as end-to-end encryption keys.
     """
-    # XXXX Most of this class should go into ClientUtils?
-    # XXXX006 Are the error messages here still reasonable?
-    # DOCDOC -- very changed.
+    # XXXX Can any more of this class should go into ClientUtils?
+    ## Fields
+    # keyring: an instance of ClientUtils.Keyring
+
+    # We discard SURB keys after 3 months.
     KEY_LIFETIME = 3*30*24*60*60
+    # We don't make new SURBs with any key that will expire in the next
+    # month.
     MIN_KEY_LIFETIME_TO_USE = 30*24*60*60
     
     def __init__(self, keyDir, passwordManager=None):
-        """DOCDOC"""
+        """Create a new ClientKeyring, storing its keys in 'keyDir'"""
         if passwordManager is None:
             passwordManager = mixminion.ClientUtils.CLIPasswordManager()
         createPrivateDir(keyDir)
+
+        # XXXX008 remove this.
+        # We used to store our keys in a different format.  At this point,
+        # it's easier to change the filename.
         obsoleteFn = os.path.join(keyDir, "keyring")
         if os.path.exists(obsoleteFn):
             LOG.warn("Ignoring obsolete keyring stored in %r",obsoleteFn)
         fn = os.path.join(keyDir, "keyring.txt")
+
+        # Setup the keyring.
         self.keyring = mixminion.ClientUtils.Keyring(fn, passwordManager)
 
     def getSURBKey(self, name="", create=0, password=None):
-        """Helper function. Return a key for a given keyid.
+        """Return a SURB key for a given identity, asking for passwords and 
+           loading the keyring if necessary..  Return None on failure.
 
-           keyid -- the name of the key.
+           name -- the SURB key identity
            create -- If true, create a new key if none is found.
-           createFn -- a callback to return a new key.
            password -- Optionally, a password for the keyring.
-           DOCDOC
         """
+        # If we haven't loaded the keyring yet, try to do so.
         if not self.keyring.isLoaded():
             try:
                 self.keyring.load(create=create,password=password)
@@ -95,7 +106,8 @@
                 return None
             if not self.keyring.isLoaded():
                 return None
-
+        
+        
         try:
             key = self.keyring.getNewestSURBKey(
                 name,minLifetime=self.MIN_KEY_LIFETIME_TO_USE)
@@ -104,11 +116,14 @@
             elif not create:
                 return None
             else:
-                # No key, we're allowed to create.
+                # No key, but we're allowed to create a new one.
                 LOG.info("Creating new key for identity %r", name)
                 return self.keyring.newSURBKey(name,
                                                time.time()+self.KEY_LIFETIME)
         finally:
+            # Check whether we changed the keyring, and save it if we did.
+            # The keyring may have changed even if we didn't generate any
+            # new keys, so this check is always necessary.
             if self.keyring.isDirty():
                 self.keyring.save()
         
@@ -215,16 +230,22 @@
                            startAt, endAt, forceQueue=0, forceNoQueue=0,
                            forceNoServerSideFragments=0):
         """Generate and send a forward message.
-            address -- the results of a parseAddress call
-            payload -- the contents of the message to send
-            servers1,servers2 -- lists of ServerInfos for the first and second
-               legs the path, respectively.
+            directory -- an instance of ClientDirectory; used to generate 
+               paths.
+            address -- an instance of ExitAddress, used to tell where to
+               deliver the message.
+            pathSpec -- an instance of PathSpec, describing the path to use.
+            message -- the contents of the message to send
+            startAt, endAt -- an interval over which all servers in the path
+               must be valid.
             forceQueue -- if true, do not try to send the message; simply
                queue it and exit.
             forceNoQueue -- if true, do not queue the message even if delivery
                fails.
-
-            DOCDOC forceNoServerSideFragments
+            forceNoServerSideFragments -- if true, and the message is too
+               large to fit in a single packet, deliver fragment packets to
+               the eventual recipient rather than having the exit server
+               defragment them.
         """
         assert not (forceQueue and forceNoQueue)
 
@@ -242,17 +263,22 @@
                          startAt, endAt, forceQueue=0,
                          forceNoQueue=0):
         """Generate and send a reply message.
-            payload -- the contents of the message to send
-            servers -- a list of ServerInfos for the first leg of the path.
-            surbList -- a list of SURBs to consider for the second leg of
-               the path.  We use the first one that is neither expired nor
-               used, and mark it used.
+            directory -- an instance of ClientDirectory; used to generate 
+               paths.
+            address -- an instance of ExitAddress, used to tell where to
+               deliver the message.
+            pathSpec -- an instance of PathSpec, describing the path to use.
+            surbList -- a list of SURBs to consider using for the reply.  We
+               use the first N that are neither expired nor used, and mark them
+               used.
+            message -- the contents of the message to send
+            startAt, endAt -- an interval over which all servers in the path
+               must be valid.
             forceQueue -- if true, do not try to send the message; simply
                queue it and exit.
             forceNoQueue -- if true, do not queue the message even if delivery
                fails.
-
-               DOCDOC args are wrong."""
+        """
         #XXXX write unit tests
         allPackets = self.generateReplyPackets(
             directory, address, pathSpec, message, surbList, startAt, endAt)
@@ -267,6 +293,7 @@
         """Generate an return a new ReplyBlock object.
             address -- the results of a parseAddress call
             servers -- lists of ServerInfos for the reply leg of the path.
+            name -- the name of the identity to use for the reply block.
             expiryTime -- if provided, a time at which the replyBlock must
                still be valid, and after which it should not be used.
         """
@@ -284,11 +311,21 @@
         """Generate packets for a forward message, but do not send
            them.  Return a list of tuples of (the packet body, a
            ServerInfo for the first hop.)
-           
-           DOCDOC
+
+            directory -- an instance of ClientDirectory; used to generate 
+               paths.
+            address -- an instance of ExitAddress, used to tell where to
+               deliver the message.
+            pathSpec -- an instance of PathSpec, describing the path to use.
+            message -- the contents of the message to send
+            noSSFragments -- if true, and the message is too large to fit in a
+               single packet, deliver fragment packets to the eventual
+               recipient rather than having the exit server defragment them.
+            startAt, endAt -- an interval over which all servers in the path
+               must be valid.
             """
-        #XXXX006 we need to factor this long-message logic out to the
-        #XXXX006 common code.  For now, this is a temporary measure.
+        #XXXX we need to factor more of this long-message logic out to the
+        #XXXX common code.  For now, this is a temporary measure.
 
         if noSSFragments:
             fragmentedMessagePrefix = ""
@@ -326,14 +363,18 @@
         """Generate a reply message, but do not send it.  Returns
            a tuple of (packet body, ServerInfo for the first hop.)
 
-            address -- the results of a parseAddress call
-            payload -- the contents of the message to send  (None for DROP
-              messages)
-            servers -- list of ServerInfo for the first leg of the path.
-            surbList -- a list of SURBs to consider for the second leg of
-               the path.  We use the first one that is neither expired nor
-               used, and mark it used.
-               DOCDOC: generates multiple packets
+
+            directory -- an instance of ClientDirectory; used to generate 
+               paths.
+            address -- an instance of ExitAddress, used to tell where to
+               deliver the message.
+            pathSpec -- an instance of PathSpec, describing the path to use.
+            message -- the contents of the message to send
+            surbList -- a list of SURBs to consider using for the reply.  We
+               use the first N that are neither expired nor used, and mark them
+               used.
+            startAt, endAt -- an interval over which all servers in the path
+               must be valid.
             """
         #XXXX write unit tests
         assert address.isReply
@@ -400,8 +441,6 @@
 
            If warnIfLost is true, log a warning if we fail to deliver
            the packets, and we don't queue them.
-
-           DOCDOC never raises
            """
         #XXXX write unit tests
         timeout = self.config['Network'].get('ConnectionTimeout')
@@ -454,11 +493,11 @@
             raise UIError(str(e))
 
     def flushQueue(self, maxPackets=None):
-        """Try to send all packets in the queue to their destinations.
-           DOCDOC maxPackets
+        """Try to send packets in the queue to their destinations.  Do not try
+           to send more than maxPackets packets.  If not all packets will be
+           sent, choose the ones to try at random.
         """
         #XXXX write unit tests
-
         class PacketProxy:
             def __init__(self,h,queue):
                 self.h = h
@@ -537,7 +576,6 @@
             LOG.info("Packet queued")
         return handles
 
-    #XXXX006 rename to decodePacket ?
     def decodeMessage(self, s, force=0, isatty=0):
         """Given a string 's' containing one or more text-encoded messages,
            return a list containing the decoded messages.
@@ -1201,20 +1239,30 @@
                              Force the client to download/not to download a
                                fresh directory.
 
-   DOCDOC Somebody needs to explain this. :)
+  -R, --recommended          Only display recommended servers.
+  -T, --with-time            Display validity intervals for server descriptors.
+  --no-collapse              Don't combine descriptors with adjacent times.
+  -s <str>,--separator=<str> Separate features with <str> instead of tab.
+  -c, --cascade              Pretty-print results, cascading by descriptors.
+  -C, --cascade-features     Pretty-print results, cascading by features.
+  -F <name>,--feature=<name> Select which server features to list.
+  --list-features            Display a list of all recognized features.
                                
 EXAMPLES:
   List all currently known servers.
       %(cmd)s
+  Same as above, but explicitly name the features to be listed.
+      %(cmd)s -F caps -F status
 """.strip()
 
 def listServers(cmd, args):
     """[Entry point] Print info about """
-    options, args = getopt.getopt(args, "hf:D:vF:TVs:cC",
+    options, args = getopt.getopt(args, "hf:D:vF:JTRs:cC",
                                   ['help', 'config=', "download-directory=",
-                                   'verbose', 'feature=', 
-                                   'with-time', "no-collapse", "valid",
-                                   "separator=", "cascade","cascade-features"])
+                                   'verbose', 'feature=', 'justify',
+                                   'with-time', "no-collapse", "recommended",
+                                   "separator=", "cascade","cascade-features",
+                                   'list-features' ])
     try:
         parser = CLIArgumentParser(options, wantConfig=1,
                                    wantClientDirectory=1,
@@ -1226,37 +1274,61 @@
     features = []
     cascade = 0
     showTime = 0
-    validOnly = 0
-    separator = "\t"
+    goodOnly = 0
+    separator = None
+    justify = 0
+    listFeatures = 0
     for opt,val in options:
         if opt in ('-F', '--feature'):
             features.extend(val.split(","))
-        elif opt == ('-T'):
-            showTime += 1
-        elif opt == ('--with-time'):
+        elif opt in ('-T', '--with-time'):
             showTime = 1
         elif opt == ('--no-collapse'):
             showTime = 2
-        elif opt in ('-V', '--valid'):
-            validOnly = 1
+        elif opt in ('-R', '--recommended'):
+            goodOnly = 1
         elif opt in ('-s', '--separator'):
             separator = val
         elif opt in ('-c', '--cascade'):
             cascade = 1
         elif opt in ('-C', '--cascade-features'):
             cascade = 2
+        elif opt in ('-J', '--justify'):
+            justify = 1
+        elif opt == ('--list-features'):
+            listFeatures = 1
+
+    if listFeatures:
+        features = mixminion.Config.getFeatureList(
+            mixminion.ServerInfo.ServerInfo)
+        features.append(("caps",))
+        features.append(("status",))
+        for f in features:
+            fCanon = f[0]
+            if len(f)>1:
+                print "%-30s (abbreviate as %s)" % (
+                    f[0], englishSequence(f[1:],compound="or"))
+            else:
+                print f[0]
+        return
 
     if not features:
-        if validOnly:
+        if goodOnly:
             features = [ 'caps' ]
         else:
             features = [ 'caps', 'status' ]
 
+    if separator is None:
+        if justify:
+            separator = ' '
+        else:
+            separator = '\t'
+
     parser.init()
     directory = parser.directory
 
     # Look up features in directory.
-    featureMap = directory.getFeatureMap(features,goodOnly=validOnly)
+    featureMap = directory.getFeatureMap(features,goodOnly=goodOnly)
 
     # If any servers are listed on the command line, restrict to those
     # servers.
@@ -1270,7 +1342,7 @@
                 lcfound[nn.lower()] = 1
         for arg in args:
             if not lcfound.has_key(arg.lower()):
-                if validOnly:
+                if goodOnly:
                     raise UIError("No recommended descriptors found for %s"%
                                   arg)
                 else:
@@ -1284,7 +1356,7 @@
 
     # Now display the result.
     for line in mixminion.ClientDirectory.formatFeatureMap(
-        features,featureMap,showTime,cascade,separator):
+        features,featureMap,showTime,cascade,separator,justify):
         print line
         
 

Index: Common.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Common.py,v
retrieving revision 1.116
retrieving revision 1.117
diff -u -d -r1.116 -r1.117
--- Common.py	7 Nov 2003 08:02:23 -0000	1.116
+++ Common.py	24 Nov 2003 19:59:04 -0000	1.117
@@ -223,9 +223,10 @@
 
 _HOST_CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
                "abcdefghijklmnopqrstuvwxyz"+
-               "0123456789.")
+               "0123456789.-")
 def isPlausibleHostname(s):
-    """DOCDOC"""
+    """Return true iff 's' is made up only of characters that sometimes
+       appear in hostnames, and has a plausible arrangement of dots."""
     if not s:
         return 0
     if s.translate(_ALLCHARS, _HOST_CHARS):
@@ -1072,14 +1073,14 @@
         gmt = time.localtime(when)
     else:
         gmt = time.gmtime(when)
-    return "%04d/%02d/%02d %02d:%02d:%02d" % (
+    return "%04d-%02d-%02d %02d:%02d:%02d" % (
         gmt[0],gmt[1],gmt[2],  gmt[3],gmt[4],gmt[5])
 
 def formatDate(when):
     """Given a time in seconds since the epoch, returns a date value in the
        format used by server descriptors (YYYY/MM/DD) in GMT"""
     gmt = time.gmtime(when+1) # Add 1 to make sure we round down.
-    return "%04d/%02d/%02d" % (gmt[0],gmt[1],gmt[2])
+    return "%04d-%02d-%02d" % (gmt[0],gmt[1],gmt[2])
 
 def formatFnameTime(when=None):
     """Given a time in seconds since the epoch, returns a date value suitable
@@ -1272,7 +1273,7 @@
         """Returns a list of (start,end) tuples for a the intervals in this
            set."""
         s = []
-        for i in range(0, len(self.edges), 2):
+        for i in xrange(0, len(self.edges), 2):
             s.append((self.edges[i][0], self.edges[i+1][0]))
         return s
 
@@ -1280,7 +1281,7 @@
         """Helper function: raises AssertionError if this set's data is
            corrupted."""
         assert (len(self.edges) % 2) == 0
-        for i in range(0, len(self.edges), 2):
+        for i in xrange(0, len(self.edges), 2):
             assert self.edges[i][0] < self.edges[i+1][0]
             assert self.edges[i][1] == '+'
             assert self.edges[i+1][1] == '-'
@@ -1578,12 +1579,19 @@
     TimeoutQueue = ClearableQueue
 else:
     class TimeoutQueue(ClearableQueue):
-        """DOCDOC -- for python 2.2 and earlier."""
+        """Helper class for Python 2.2. and earlier: extends the 'get'
+           functionality of Queue.Queue to support a 'timeout' argument.
+           If 'block' is true and timeout is provided, wait for no more
+           than 'timeout' seconds before raising QueueEmpty.
+           
+           In Python 2.3 and later, this interface is standard.
+        """
         def get(self, block=1, timeout=None):
-            if timeout is None:
+            if timeout is None or not block:
                 return MessageQueue.get(self, block)
 
-            # Adapted from 'Condition'.
+            # This logic is adapted from 'Condition' in the Python 
+            # threading module.
             _time = time.time
             _sleep = time.sleep
             deadline = timeout+_time()

Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.67
retrieving revision 1.68
diff -u -d -r1.67 -r1.68
--- Config.py	20 Nov 2003 08:48:33 -0000	1.67
+++ Config.py	24 Nov 2003 19:59:04 -0000	1.68
@@ -349,8 +349,7 @@
         raise ConfigError("Invalid exponent on public key")
     return key
 
-# FFFF006 begin generating YYYY-MM-DD
-# FFFF007 stop accepting YYYYY/MM/DD
+# FFFF007/8 stop accepting YYYY/MM/DD
 # Regular expression to match YYYY/MM/DD or YYYY-MM-DD
 _date_re = re.compile(r"^(\d\d\d\d)([/-])(\d\d)([/-])(\d\d)$")
 def _parseDate(s):
@@ -370,9 +369,7 @@
         raise ConfigError("Invalid date %r"%s)
     return calendar.timegm((yyyy,MM,dd,0,0,0,0,0,0))
 
-
-# FFFF006 begin generating YYYY-MM-DD
-# FFFF007 stop accepting YYYYY/MM/DD
+# FFFF007/8 stop accepting YYYY/MM/DD
 # Regular expression to match YYYY/MM/DD HH:MM:SS
 _time_re = re.compile(r"^(\d\d\d\d)([/-])(\d\d)([/-])(\d\d)\s+"
                       r"(\d\d):(\d\d):(\d\d)((?:\.\d\d\d)?)$")
@@ -626,6 +623,28 @@
                           name, englishSequence(secs,compound="or")))
         else:
             return result[0]
+
+def getFeatureList(klass):
+    """Get a list of all feature names from the _ConfigFile subclass
+       'klass'.  Return a list of tuples, each of which contains all the
+       synonyms for a single feature."""
+    syn = klass._syntax
+    features = []
+    for secname, secitems in syn.items():
+        for entname in secitems.keys():
+            if entname.startswith("__"): continue
+            synonyms = []
+            synonyms.append("%s:%s"%(secname,entname))
+            unique = 1
+            for sn, si in syn.items():
+                if sn != secname and si.has_key(entname):
+                    unique = 0
+                    break
+            if unique:
+                synonyms.append(entname)
+            features.append(tuple(synonyms))
+    features.sort()
+    return features
 
 class _ConfigFile:
     """Base class to parse, validate, and represent configuration files.

Index: MMTPClient.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/MMTPClient.py,v
retrieving revision 1.42
retrieving revision 1.43
diff -u -d -r1.42 -r1.43
--- MMTPClient.py	19 Nov 2003 09:48:09 -0000	1.42
+++ MMTPClient.py	24 Nov 2003 19:59:04 -0000	1.43
@@ -35,6 +35,7 @@
     # targetKeyID -- sha1 hash of the ASN1 encoding of the public key we
     #   expect the server to use, or None if we don't care.
     # context: a TLSContext object; used to create connections.
+    # serverName: The name of the server to display in log messages.
     # sock: a TCP socket, open to the server.
     # tls: a TLS socket, wrapping sock.
     # protocol: The MMTP protocol version we're currently using, or None
@@ -58,7 +59,6 @@
         self.certCache = PeerCertificateCache()
         if serverName:
             self.serverName = serverName
-            #DOCDOC
         else:
             self.serverName = mixminion.ServerInfo.displayServer(
                 mixminion.Packet.IPV4Info(targetAddr, targetPort, targetKeyID))
@@ -283,11 +283,11 @@
     def __init__(self):
         self.cache = {}
 
-    #XXXX006 use displayName to 
     def check(self, tls, targetKeyID, serverName):
         """Check whether the certificate chain on the TLS connection 'tls'
            is valid, current, and matches the keyID 'targetKeyID'.  If so,
-           return.  If not, raise MixProtocolBadAuth.
+           return.  If not, raise MixProtocolBadAuth.  Display all messages
+           using the server 'serverName'.
         """
         
         # First, make sure the certificate is neither premature nor expired.
@@ -304,6 +304,7 @@
 
         # Get the KeyID for the peer (temporary) key.
         hashed_peer_pk = sha1(tls.get_peer_cert_pk().encode_key(public=1))
+
         # Before 0.0.4alpha, a server's keyID was a hash of its current
         # TLS public key.  In 0.0.4alpha, we allowed this for backward
         # compatibility.  As of 0.0.4alpha2, since we've dropped backward

Index: Main.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Main.py,v
retrieving revision 1.61
retrieving revision 1.62
diff -u -d -r1.61 -r1.62
--- Main.py	19 Nov 2003 09:48:09 -0000	1.61
+++ Main.py	24 Nov 2003 19:59:04 -0000	1.62
@@ -141,11 +141,6 @@
     "dir":             ( 'mixminion.directory.DirMain', 'main'),
 
     "shell":           ( 'mixminion.Main',       'commandShell' ),
-    
-    # XXXX006 Obsolete commands.  Remove in 0.0.6
-    "server" :         ( 'mixminion.Main', 'rejectCommand' ),
-    "inspect-pool" :   ( 'mixminion.Main', 'rejectCommand' ),
-    "pool" :           ( 'mixminion.Main', 'rejectCommand' ),
 }
 
 _USAGE = (
@@ -191,11 +186,11 @@
     print "      to be anonymous, and the code is too alpha to be reliable."
 
 def rejectCommand(cmd,args):
+    # This function gets called when we have an obsolete command 'cmd'.
+    # First, let's see whether we know an updated equivalent or not.
     cmd = cmd.split()[-1]
-    cmdDict = { "client" : "send",
-                "pool" : "queue",
-                "inspect-pool" : "inspect-queue",
-                "server" : "server-start" }
+    # Map from obsolete commands to current versions.
+    cmdDict = { } # Right now, there aren't any obsolete commands still in use.
     newCmd = cmdDict.get(cmd)
     if newCmd:
         print "The command %r is obsolete.  Use %r instead."%(cmd,newCmd)
@@ -210,6 +205,8 @@
     print "      to be anonymous, and the code is too alpha to be reliable."
 
 def commandShell(cmd,args):
+    # Used to implement a 'mixminion shell' on systems (like windows) with
+    # somewhat bogus CLI support. 
     import mixminion
     import shlex
 

Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.65
retrieving revision 1.66
diff -u -d -r1.65 -r1.66
--- Packet.py	19 Nov 2003 09:48:09 -0000	1.65
+++ Packet.py	24 Nov 2003 19:59:04 -0000	1.66
@@ -94,9 +94,9 @@
 MAX_EXIT_TYPE  = 0xFFFF
 
 # Set of exit types that don't get tag fields. 
-# XXXX006 This interface is really brittle; it needs to change.  I added it 
-# XXXX006 in order to allow 'fragment' to be an exit type without adding a
-# XXXX006 needless tag field to every fragment routing info.  
+# XXXX007 This interface is really brittle; it needs to change.  I added it 
+# XXXX007 in order to allow 'fragment' to be an exit type without adding a
+# XXXX007 needless tag field to every fragment routing info.  
 _TYPES_WITHOUT_TAGS = { FRAGMENT_TYPE : 1 }
 
 def typeIsSwap(tp):
@@ -205,7 +205,7 @@
         """Return the part of the routingInfo that contains the delivery
            address.  (Requires that routingType is an exit type.)"""
         assert self.routingtype >= MIN_EXIT_TYPE
-        #XXXX006 This interface is completely insane.  Change it.
+        #XXXX007 This interface is completely insane.  Change it.
         if _TYPES_WITHOUT_TAGS.get(self.routingtype):
             return self.routinginfo
         else:
@@ -216,7 +216,7 @@
         """Return the part of the routingInfo that contains the decoding
            tag. (Requires that routingType is an exit type.)"""
         assert self.routingtype >= MIN_EXIT_TYPE
-        #XXXX006 This interface is completely insane.  Change it.
+        #XXXX007 This interface is completely insane.  Change it.
         if _TYPES_WITHOUT_TAGS.get(self.routingtype):
             return ""
         else:
@@ -363,9 +363,6 @@
 
 class FragmentPayload(_Payload):
     """Represents the fields of a decoded fragment payload.
-
-       FFFF Fragments are not yet fully supported; there's no code to generate
-            or decode them.
     """
     def __init__(self, index, hash, msgID, msgLen, data):
         self.index = index

Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.63
retrieving revision 1.64
diff -u -d -r1.63 -r1.64
--- ServerInfo.py	20 Nov 2003 08:49:20 -0000	1.63
+++ ServerInfo.py	24 Nov 2003 19:59:04 -0000	1.64
@@ -42,7 +42,10 @@
 
 # ----------------------------------------------------------------------
 def displayServer(s):
-    """DOCDOC"""
+    """Return the best possible human-readable name for a server 's'.
+       's' must be one of: None, IPV4Info, MMTPHostInfo, ServerInfo,
+       string.
+    """
     #XXXX006 unit tests are needed
     if isinstance(s, types.StringType):
         return s
@@ -71,13 +74,22 @@
     return "%s at %s" % (nickname, addr)
 
 def getNicknameByKeyID(keyid):
-    """DOCDOC"""
+    """Given a 20-byte keyid, look up the nickname of the corresponding
+       server.  Return the nickname on success and None if we don't recognize
+       the server.
+    
+       FFFF Right now, this is not supported for servers, since they don't
+       FFFF download directories.
+    """
+    #FFFF Be cleverer with all-zero keyids.
     if _keyIDToNicknameFn:
         return _keyIDToNicknameFn(keyid)
     else:
         return None
 
-#DOCDOC
+# This variable should hold None, or a function that maps from keyids to
+# nicknames, and returns None on failure.  Currently set by
+# ClientDirectory.ClientDirectory._installAsKeyIDResolver().
 _keyIDToNicknameFn = None
 
 # ----------------------------------------------------------------------
@@ -107,8 +119,8 @@
                      "Comments": ("ALLOW", None, None),
                      "Packet-Key": ("REQUIRE", "publicKey", None),
                      "Contact-Fingerprint": ("ALLOW", None, None),
-                     # XXXX010 change these next few to "REQUIRE".
                      "Packet-Formats": ("ALLOW", None, None),#XXXX007 remove
+                     # XXXX010 change these next few to "REQUIRE".
                      "Packet-Versions": ("ALLOW", None, None),
                      "Software": ("ALLOW", None, None),
                      "Secure-Configuration": ("ALLOW", "boolean", None),
@@ -116,10 +128,10 @@
                      },
         "Incoming/MMTP" : {
                      "Version": ("REQUIRE", None, None),
-                     "IP": ("ALLOW", "IP", None),#XXXX007 remove
+                     "IP": ("ALLOW", "IP", None),#XXXX007/8 remove
                      "Hostname": ("ALLOW", "host", None),#XXXX008 require
                      "Port": ("REQUIRE", "int", None),
-                     "Key-Digest": ("ALLOW", "base64", None),#XXXX007 rmv
+                     "Key-Digest": ("ALLOW", "base64", None),#XXXX007/8 rmv
                      "Protocols": ("REQUIRE", None, None),
                      "Allow": ("ALLOW*", "addressSet_allow", None),
                      "Deny": ("ALLOW*", "addressSet_deny", None),

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.166
retrieving revision 1.167
diff -u -d -r1.166 -r1.167
--- test.py	20 Nov 2003 08:51:28 -0000	1.166
+++ test.py	24 Nov 2003 19:59:04 -0000	1.167
@@ -34,7 +34,9 @@
 except ImportError:
     import mixminion._unittest as unittest
 
-#DOCDOC
+# If the environment variable MM_COVERAGE is set, then start running the
+# coverage analysis tool.  You'll need to install the file 'coverage.py':
+# it's included in the contrib directory.
 if os.environ.get("MM_COVERAGE"):
     import coverage
     coverage.erase()
@@ -132,7 +134,10 @@
 #----------------------------------------------------------------------
 # DNS override
 def overrideDNS(overrideDict,delay=0):
-    """DOCDOC"""
+    """Helper function: temporarily replace the DNS functionality in
+       NetUtils to lookup entries in overrideDict instead of on the
+       network, and pause for 'delay' seconds before returning an answer.
+    """
     def getIPs_replacement(addr,overrideDict=overrideDict,delay=delay):
         v = overrideDict.get(addr)
         if delay: time.sleep(delay)
@@ -5938,15 +5943,22 @@
                               (socket.AF_INET, '10.2.4.11'))
             self.assertEquals(receiveDict['bar'][:2],
                               (mixminion.NetUtils.AF_INET6, '18:0FFF::4:1'))
-            self.assert_(DELAY <= receiveDict['foo'][2]-start <= DELAY+LATENCY)
-            self.assert_(DELAY <= receiveDict['bar'][2]-start <= DELAY+LATENCY)
+            # We allow a little wiggle on DELAY, since many OS's don't
+            # stricly guarantee that 
+            #     t1=time();sleep(x);t2=time();assert t2>=t1+x
+            # will pass.
+            self.assert_(DELAY*.8 <= receiveDict['foo'][2]-start 
+                                  <= DELAY+LATENCY)
+            self.assert_(DELAY*.8 <= receiveDict['bar'][2]-start 
+                                  <= DELAY+LATENCY)
             self.assertEquals(receiveDict['nowhere.noplace'][0], "NOENT")
             self.assertEquals(cache.getNonblocking("foo"),
                               receiveDict['foo'])
             self.assertEquals(cache.getNonblocking("baz.com")[:2],
                               (socket.AF_INET, '10.99.22.8'))
-            self.assert_(DELAY*1.25 <= receiveDict['baz.com'][2]-start <= DELAY*1.24 + LATENCY)
-            cache.cleanCache(receiveDict['foo'][2]+
+            self.assert_(DELAY*1.20 <= receiveDict['baz.com'][2]-start 
+                                    <= DELAY*1.25 + LATENCY)
+            cache.cleanCache(start+DELAY+
                              mixminion.server.DNSFarm.MAX_ENTRY_TTL+.001)
             self.assertEquals(cache.getNonblocking('foo'), None)
             self.assertEquals(cache.getNonblocking('nowhere.noplace'),
@@ -6751,6 +6763,8 @@
         eq((len(p1),len(p2)), (1,5))
 
         # 1d'. Tilde
+        p1,p2 = ppath(ks, None, '~2', email)
+        self.assert_(p1 and p2)
         p1,p2 = ppath(ks, None, '?,~4,Bob,Joe', email) #default nHops=6
         p = p1+p2
         pathIs((p2[-1], p2[-2],), (joe, bob))
@@ -6835,38 +6849,42 @@
         self.assertEquals(fm['Bob'].values()[0],
                           { 'software' : 'Mixminion %s' %mixminion.__version__,
                             'caps' : 'relay',
-                            'status' : '(not recommended)' })
+                            'status' : '(ok)' })
 
         # Now let's try compressing.
-        fm = { 'Alice' : { (100,200) : { "A" : "xx", "B" : "yy" },
-                           (200,300) : { "A" : "xx", "B" : "yy" },
-                           (350,400) : { "A" : "xx", "B" : "yy" } },
+        fm = { 'Alice' : { (100,200) : { "A" : "xxx", "B" : "yy" },
+                           (200,300) : { "A" : "xxx", "B" : "yy" },
+                           (350,400) : { "A" : "xxx", "B" : "yy" } },
                'Bob'   : { (100,200) : { "A" : "zz", "B" : "ww" },
                            (200,300) : { "A" : "zz", "B" : "kk" } } }
         fm2 = compressFeatureMap(fm, ignoreGaps=0, terse=0)
         self.assertEquals(fm2,
-            { 'Alice' : { (100,300) : { "A" : "xx", "B" : "yy" },
-                          (350,400) : { "A" : "xx", "B" : "yy" } },
+            { 'Alice' : { (100,300) : { "A" : "xxx", "B" : "yy" },
+                          (350,400) : { "A" : "xxx", "B" : "yy" } },
               'Bob'   : { (100,200) : { "A" : "zz", "B" : "ww" },
                           (200,300) : { "A" : "zz", "B" : "kk" } } })
         fm3 = compressFeatureMap(fm, ignoreGaps=1, terse=0)
         self.assertEquals(fm3,
-            { 'Alice' : { (100,400) :  { "A" : "xx", "B" : "yy" } },
+            { 'Alice' : { (100,400) :  { "A" : "xxx", "B" : "yy" } },
               'Bob'   : { (100,200) : { "A" : "zz", "B" : "ww" },
                           (200,300) : { "A" : "zz", "B" : "kk" } } })
 
         fm4 = compressFeatureMap(fm, terse=1)
         self.assertEquals(fm4,
-            { 'Alice' : { (100,400) : { "A" : "xx", "B" : "yy" } },
+            { 'Alice' : { (100,400) : { "A" : "xxx", "B" : "yy" } },
               'Bob'   : { (100,300) : { "A" : "zz", "B" : "ww / kk" } } })
 
         # Test formatFeatureMap.
         self.assertEquals(formatFeatureMap(["A","B"],fm4,showTime=0,cascade=0),
-          [ "Alice:xx yy", "Bob:zz ww / kk" ])
+          [ "Alice:xxx yy", "Bob:zz ww / kk" ])
+        self.assertEquals(formatFeatureMap(["A","B"],fm4,showTime=0,cascade=0,
+                                           just=1,sep="!"),
+          [ "Alice:xxx!yy     ", 
+            "Bob  :zz !ww / kk" ])
         self.assertEquals(formatFeatureMap(["A","B"],fm3,showTime=1,cascade=0),
-          [ "Alice:1970/01/01 to 1970/01/01:xx yy",
-            "Bob:1970/01/01 to 1970/01/01:zz ww",
-            "Bob:1970/01/01 to 1970/01/01:zz kk" ])
+          [ "Alice:1970-01-01 to 1970-01-01:xxx yy",
+            "Bob:1970-01-01 to 1970-01-01:zz ww",
+            "Bob:1970-01-01 to 1970-01-01:zz kk" ])
 
         day1 = 24*60*60
         day2 = 2*24*60*60
@@ -6875,16 +6893,16 @@
                 'Bob' : { (day1,day2) : { "A" : "a1", "B" : "b1" },
                           (day2,day3) : { "A" : "a2", "B" : "b2" } } }
         self.assertEquals(formatFeatureMap(["A","B"],fmx,cascade=1,sep="##"),
-          [ "Alice:", "  [1970/01/02 to 1970/01/04] aa##bb",
+          [ "Alice:", "  [1970-01-02 to 1970-01-04] aa##bb",
             "Bob:",
-            "  [1970/01/02 to 1970/01/03] a1##b1",
-            "  [1970/01/03 to 1970/01/04] a2##b2" ])
+            "  [1970-01-02 to 1970-01-03] a1##b1",
+            "  [1970-01-03 to 1970-01-04] a2##b2" ])
         
         self.assertEquals(formatFeatureMap(["A","B"],fmx,showTime=1,cascade=2),
-          [ "Alice:", "  [1970/01/02 to 1970/01/04]", "    A:aa", "    B:bb",
+          [ "Alice:", "  [1970-01-02 to 1970-01-04]", "    A:aa", "    B:bb",
             "Bob:",
-            "  [1970/01/02 to 1970/01/03]", "    A:a1", "    B:b1",
-            "  [1970/01/03 to 1970/01/04]", "    A:a2", "    B:b2" ])
+            "  [1970-01-02 to 1970-01-03]", "    A:a1", "    B:b1",
+            "  [1970-01-03 to 1970-01-04]", "    A:a2", "    B:b2" ])
 
     def writeDescriptorsToDisk(self):
         edesc = getExampleServerDescriptors()
@@ -7364,7 +7382,7 @@
     tc = loader.loadTestsFromTestCase
 
     if 0:
-        suite.addTest(tc(ClientUtilTests))
+        suite.addTest(tc(DNSFarmTests))
         return suite
     testClasses = [MiscTests,
                    MinionlibCryptoTests,
@@ -7406,7 +7424,8 @@
     initializeGlobals()
     unittest.TextTestRunner(verbosity=1).run(testSuite())
 
-    #DOCDOC
+    # If we're doing coverage analysis, then report on all the modules
+    # under the mixminion package.
     if os.environ.get("MM_COVERAGE"):
         allmods = [ mod for name, mod in sys.modules.items()
                     if (mod is not None and