Fixed docstrings so only Sphinx errors are "autosummary" and "missing attribute"
authorW. Trevor King <wking@drexel.edu>
Sun, 7 Feb 2010 22:53:53 +0000 (17:53 -0500)
committerW. Trevor King <wking@drexel.edu>
Sun, 7 Feb 2010 22:53:53 +0000 (17:53 -0500)
25 files changed:
doc/generate-libbe-txt.py
doc/ids.txt [deleted file]
doc/index.txt
doc/tutorial.txt
libbe/__init__.py
libbe/bugdir.py
libbe/command/serve.py
libbe/diff.py
libbe/storage/__init__.py
libbe/storage/base.py
libbe/storage/http.py
libbe/storage/util/config.py
libbe/storage/util/mapfile.py
libbe/storage/util/properties.py
libbe/storage/util/settings_object.py
libbe/storage/vcs/__init__.py
libbe/storage/vcs/arch.py
libbe/storage/vcs/base.py
libbe/storage/vcs/bzr.py
libbe/storage/vcs/darcs.py
libbe/storage/vcs/git.py
libbe/storage/vcs/hg.py
libbe/util/id.py
libbe/util/tree.py
libbe/util/utility.py

index ec874fad6f74783fb8ab9cfc89e76aed018fa2bf..35eb5c4fb2cdd9603c599f5226d2c93fa22bef08 100644 (file)
@@ -31,7 +31,7 @@ def toctree(children):
             '   :maxdepth: 2',
             '',
             ] + [
-            '   %s.txt' % c for c in children
+            '   %s.txt' % c for c in sorted(children)
             ] + ['', ''])
 
 def make_module_txt(modname, children):
diff --git a/doc/ids.txt b/doc/ids.txt
deleted file mode 100644 (file)
index ba1837b..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-**********
-Object IDs
-**********
-
-Format
-======
-
-BE IDs are formatted::
-
-    <bug-directory>[/<bug>[/<comment>]]
-
-where each ``<..>`` is a UUID.  For example::
-
-    bea86499-824e-4e77-b085-2d581fa9ccab/3438b72c-6244-4f1d-8722-8c8d41484e35
-
-refers to bug ``3438b72c-6244-4f1d-8722-8c8d41484e35`` which is
-located in bug directory ``bea86499-824e-4e77-b085-2d581fa9ccab``.
-This is a bit of a mouthful, so you can truncate each UUID so long as
-it remains unique.  For example::
-
-    bea/343
-
-If there were two bugs ``3438...`` and ``343a...`` in ``bea``, you'd
-have to use::
-
-    bea/3438
-
-BE will only truncate each UUID down to three characters to slightly
-future-proof the short user ids.  However, if you want to save keystrokes
-and you *know* there is only one bug directory, feel free to truncate
-all the way to zero characters::
-
-    /3438
-
-Cross references
-================
-
-To refer to other bug-directories/bugs/comments from bug comments, simply
-enclose the ID in pound signs (``#``).  BE will automatically expand the
-truncations to the full UUIDs before storing the comment, and the reference
-will be appropriately truncated (and hyperlinked, if possible) when the
-comment is displayed.
-
-Scope
-=====
-
-Although bug and comment IDs always appear in compound references,
-UUIDs at each level are globally unique.  For example, comment
-``bea/343/ba96f1c0-ba48-4df8-aaf0-4e3a3144fc46`` will *only* appear
-under ``bea/343``.  The prefix (``bea/343``) allows BE to reduce
-caching global comment-lookup tables and enables easy error messages
-("I couldn't find ``bea/343/ba9`` because I don't know where the
-``bea`` bug directory is located").
index 6765a6892f61081d84d944052b783be11ce6b16c..30b0318b06e517ef9f92c472aa0a5f565a8ce8f6 100644 (file)
@@ -23,7 +23,6 @@ Contents:
 
    install.txt
    tutorial.txt
-   ids.txt
    email.txt
    html.txt
    distributed_bugtracking.txt
index 3dd7ff3451583c60196f2140e1fd1192c2714600..7932c9cfc27f4e7fad66d0b33bbcd0fb389517a6 100644 (file)
@@ -24,7 +24,7 @@ powerful, and leave the web and email interfaces to other documents.
 .. _command-line: `Command-line interface`_
 .. _web: tutorial-html.txt
 .. _email: tutorial-email.txt
-.. _IDs: ids.txt
+.. _IDs: libbe/libbe.util.id.txt
 
 Installation
 ============
index 23acfef52a6b329aa778419e79f657141add90af..d32716fe194db3e53ca8176a6fc9b854d7f85edb 100644 (file)
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-# To reduce module load time, test suite generation is turned of by
-# default.  If you _do_ want to generate the test suites, set
-# TESTING=True before loading any libbe or becommands submodules.
+"""The libbe module does all the legwork for bugs-everywhere_ (BE).
+
+.. _bugs-everywhere: http://bugseverywhere.org
+
+To facilitate faster loading, submodules are not imported by default.
+The available submodules are:
+
+* :mod:`libbe.bugdir`
+* :mod:`libbe.bug`
+* :mod:`libbe.comment`
+* :mod:`libbe.command`
+* :mod:`libbe.diff`
+* :mod:`libbe.error`
+* :mod:`libbe.storage`
+* :mod:`libbe.ui`
+* :mod:`libbe.util`
+* :mod:`libbe.version`
+* :mod:`libbe._version`
+"""
+
 TESTING = False
+"""Flag controlling test-suite generation.
+
+To reduce module load time, test suite generation is turned of by
+default.  If you *do* want to generate the test suites, set
+``TESTING=True`` before loading any :mod:`libbe` submodules.
+
+Examples
+--------
+
+>>> import libbe
+>>> libbe.TESTING = True
+>>> import libbe.bugdir
+>>> 'SimpleBugDir' in dir(libbe.bugdir)
+True
+"""
index 9328b064270fd9bf739fe6da6168bcb3c487ba22..65136febf06b75143ecb97f4903f0dba76c657a7 100644 (file)
@@ -48,31 +48,6 @@ if libbe.TESTING == True:
     import libbe.storage.base
 
 
-class NoBugDir(Exception):
-    def __init__(self, path):
-        msg = "The directory \"%s\" has no bug directory." % path
-        Exception.__init__(self, msg)
-        self.path = path
-
-class NoRootEntry(Exception):
-    def __init__(self, path):
-        self.path = path
-        Exception.__init__(self, "Specified root does not exist: %s" % path)
-
-class AlreadyInitialized(Exception):
-    def __init__(self, path):
-        self.path = path
-        Exception.__init__(self,
-                           "Specified root is already initialized: %s" % path)
-
-class MultipleBugMatches(ValueError):
-    def __init__(self, shortname, matches):
-        msg = ("More than one bug matches %s.  "
-               "Please be more specific.\n%s" % (shortname, matches))
-        ValueError.__init__(self, msg)
-        self.shortname = shortname
-        self.matches = matches
-
 class NoBugMatches(libbe.util.id.NoIDMatches):
     def __init__(self, *args, **kwargs):
         libbe.util.id.NoIDMatches.__init__(self, *args, **kwargs)
@@ -81,17 +56,27 @@ class NoBugMatches(libbe.util.id.NoIDMatches):
             return 'No bug matches %s' % self.id
         return self.msg
 
-class DiskAccessRequired (Exception):
-    def __init__(self, goal):
-        msg = "Cannot %s without accessing the disk" % goal
-        Exception.__init__(self, msg)
-
 
 class BugDir (list, settings_object.SavedSettingsObject):
     """A BugDir is a container for :class:`~libbe.bug.Bug`\s, with some
     additional attributes.
 
-    See :class:`SimpleBugDir` for some bugdir manipulation exampes.
+    Parameters
+    ----------
+    storage : :class:`~libbe.storage.base.Storage`
+       Storage instance containing the bug directory.  If
+       `from_storage` is `False`, `storage` may be `None`.
+    uuid : str, optional
+       Set the bugdir UUID (see :mod:`libbe.util.id`).
+       Useful if you are loading one of several bugdirs
+       stored in a single Storage instance.
+    from_storage : bool, optional
+       If `True`, attempt to load from storage.  Otherwise,
+       setup in memory, saving to `storage` if it is not `None`.
+
+    See Also
+    --------
+    :class:`SimpleBugDir` for some bugdir manipulation exampes.
     """
 
     settings_properties = []
index 7a98a47a5708def3892107b0e5dd2d2356c13119..72373434500f721bd5f5bb8b6ac14f475924052b 100644 (file)
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
+"""Define the :class:`Serve` serving BE Storage over HTTP.
+
+See Also
+--------
+:mod:`libbe.storage.http` : the associated client
+"""
+
 import hashlib
 import logging
 import os.path
@@ -156,9 +163,10 @@ class Users (dict):
 
 class WSGI_Object (object):
     """Utility class for WGSI clients and middleware.
+
     For details on WGSI, see `PEP 333`_
 
-    .. PEP 333: http://www.python.org/dev/peps/pep-0333/
+    .. _PEP 333: http://www.python.org/dev/peps/pep-0333/
     """
     def __init__(self, logger=None, log_level=logging.INFO, log_format=None):
         self.logger = logger
@@ -223,6 +231,7 @@ class WSGI_Object (object):
 
 class ExceptionApp (WSGI_Object):
     """Some servers (e.g. cherrypy) eat app-raised exceptions.
+
     Work around that by logging tracebacks by hand.
     """
     def __init__(self, app, *args, **kwargs):
@@ -242,7 +251,9 @@ class ExceptionApp (WSGI_Object):
             raise
 
 class UppercaseHeaderApp (WSGI_Object):
-    """From PEP 333, `The start_response() Callable`_ :
+    """WSGI middleware that uppercases incoming HTTP headers.
+
+    From PEP 333, `The start_response() Callable`_ :
 
         A reminder for server/gateway authors: HTTP
         header names are case-insensitive, so be sure
@@ -291,7 +302,7 @@ class AuthenticationApp (WSGI_Object):
                               e.code, e.msg, e.headers)
 
     def authenticate(self, environ):
-        """Handle user-authentication sent in the 'Authorization' header.
+        """Handle user-authentication sent in the "Authorization" header.
         
         This function implements ``Basic`` authentication as described in
         HTTP/1.0 specification [1]_ .  Do not use this module unless you
@@ -299,6 +310,9 @@ class AuthenticationApp (WSGI_Object):
 
         .. [1] http://www.w3.org/Protocols/HTTP/1.0/draft-ietf-http-spec.html#BasicAA
 
+        Examples
+        --------
+
         >>> users = Users()
         >>> users.add_user(User('Aladdin', 'Big Al', password='open sesame'))
         >>> app = AuthenticationApp(app=None, realm='Dummy Realm', users=users)
@@ -306,6 +320,9 @@ class AuthenticationApp (WSGI_Object):
         'Aladdin'
         >>> app.authenticate({'HTTP_AUTHORIZATION':'Basic AAAAAAAAAAAAAAAAAAAAAAAAAA=='})
 
+        Notes
+        -----
+
         Code based on authkit/authenticate/basic.py
         (c) 2005 Clark C. Evans.
         Released under the MIT License:
@@ -339,8 +356,7 @@ class AuthenticationApp (WSGI_Object):
         return False
 
 class WSGI_AppObject (WSGI_Object):
-    """Utility class for WGSI clients and middleware with
-    useful utilities for handling data (POST, QUERY) and
+    """Useful WSGI utilities for handling data (POST, QUERY) and
     returning responses.
     """
     def __init__(self, *args, **kwargs):
@@ -469,10 +485,12 @@ class AdminApp (WSGI_AppObject):
         return self.ok_response(environ, start_response, None)
 
 class ServerApp (WSGI_AppObject):
-    """RESTful_ WSGI request handler for serving the
+    """WSGI server for a BE Storage instance over HTTP.
+
+    RESTful_ WSGI request handler for serving the
     libbe.storage.http.HTTP backend with GET, POST, and HEAD commands.
-    For more information on authentication and REST, see John Calcote's
-    `Open Sourcery article`_
+    For more information on authentication and REST, see John
+    Calcote's `Open Sourcery article`_
 
     .. _RESTful: http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
     .. _Open Sourcery article: http://jcalcote.wordpress.com/2009/08/10/restful-authentication/
@@ -480,6 +498,9 @@ class ServerApp (WSGI_AppObject):
     This serves files from a connected storage instance, usually
     a VCS-based repository located on the local machine.
 
+    Notes
+    -----
+
     The GET and HEAD requests are identical except that the HEAD
     request omits the actual content of the file.
     """
@@ -505,10 +526,12 @@ class ServerApp (WSGI_AppObject):
             ]
 
     def __call__(self, environ, start_response):
-        """The main WSGI application.  Dispatch the current request to
-        the functions from above and store the regular expression
-        captures in the WSGI environment as `be-server.url_args` so
-        that the functions from above can access the url placeholders.
+        """The main WSGI application.
+
+        Dispatch the current request to the functions from above and
+        store the regular expression captures in the WSGI environment
+        as `be-server.url_args` so that the functions from above can
+        access the url placeholders.
 
         URL dispatcher from Armin Ronacher's "Getting Started with WSGI"
           http://lucumr.pocoo.org/2007/5/21/getting-started-with-wsgi
@@ -678,7 +701,8 @@ class ServerApp (WSGI_AppObject):
 
 
 class Serve (libbe.command.Command):
-    """Serve a Storage backend for the HTTP storage client
+    """:class:`~libbe.command.base.Command` wrapper around
+    :class:`ServerApp`.
     """
 
     name = 'serve'
@@ -1041,33 +1065,45 @@ def get_cert_filenames(server_name, autogenerate=True, logger=None):
     return (pkey_file, cert_file)
 
 def createKeyPair(type, bits):
-    """
-    Create a public/private key pair.
+    """Create a public/private key pair.
+
+    Returns the public/private key pair in a PKey object.
 
-    Arguments: type - Key type, must be one of TYPE_RSA and TYPE_DSA
-               bits - Number of bits to use in the key
-    Returns:   The public/private key pair in a PKey object
+    Parameters
+    ----------
+    type : TYPE_RSA or TYPE_DSA
+      Key type.
+    bits : int
+      Number of bits to use in the key.
     """
     pkey = OpenSSL.crypto.PKey()
     pkey.generate_key(type, bits)
     return pkey
 
 def createCertRequest(pkey, digest="md5", **name):
-    """
-    Create a certificate request.
-
-    Arguments: pkey   - The key to associate with the request
-               digest - Digestion method to use for signing, default is md5
-               **name - The name of the subject of the request, possible
-                        arguments are:
-                          C     - Country name
-                          ST    - State or province name
-                          L     - Locality name
-                          O     - Organization name
-                          OU    - Organizational unit name
-                          CN    - Common name
-                          emailAddress - E-mail address
-    Returns:   The certificate request in an X509Req object
+    """Create a certificate request.
+
+    Returns the certificate request in an X509Req object.
+
+    Parameters
+    ----------
+    pkey : PKey
+      The key to associate with the request.
+    digest : "md5" or ?
+      Digestion method to use for signing, default is "md5",
+    `**name` :
+      The name of the subject of the request, possible.
+      Arguments are:
+
+      ============ ========================
+      C            Country name
+      ST           State or province name
+      L            Locality name
+      O            Organization name
+      OU           Organizational unit name
+      CN           Common name
+      emailAddress E-mail address
+      ============ ========================
     """
     req = OpenSSL.crypto.X509Req()
     subj = req.get_subject()
@@ -1080,19 +1116,28 @@ def createCertRequest(pkey, digest="md5", **name):
     return req
 
 def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter), digest="md5"):
-    """
-    Generate a certificate given a certificate request.
-
-    Arguments: req        - Certificate reqeust to use
-               issuerCert - The certificate of the issuer
-               issuerKey  - The private key of the issuer
-               serial     - Serial number for the certificate
-               notBefore  - Timestamp (relative to now) when the certificate
-                            starts being valid
-               notAfter   - Timestamp (relative to now) when the certificate
-                            stops being valid
-               digest     - Digest method to use for signing, default is md5
-    Returns:   The signed certificate in an X509 object
+    """Generate a certificate given a certificate request.
+
+    Returns the signed certificate in an X509 object.
+
+    Parameters
+    ----------
+    req :
+      Certificate reqeust to use
+    issuerCert :
+      The certificate of the issuer
+    issuerKey :
+      The private key of the issuer
+    serial :
+      Serial number for the certificate
+    notBefore :
+      Timestamp (relative to now) when the certificate
+      starts being valid
+    notAfter :
+      Timestamp (relative to now) when the certificate
+      stops being valid
+    digest :
+      Digest method to use for signing, default is md5
     """
     cert = OpenSSL.crypto.X509()
     cert.set_serial_number(serial)
@@ -1105,9 +1150,9 @@ def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter
     return cert
 
 def make_certs(server_name, logger=None) :
-    """
-    Generate private key and certification files.
-    mk_certs(server_name) -> (pkey_filename, cert_filename)
+    """Generate private key and certification files.
+
+    `mk_certs(server_name) -> (pkey_filename, cert_filename)`
     """
     if OpenSSL == None:
         raise libbe.command.UserError, \
index 35e2151207c612a235bb6997670079686092d3fc..dc13b6115fd92aa47b381df2dd4df532192cb18d 100644 (file)
@@ -16,7 +16,8 @@
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-"""Compare two bug trees."""
+"""Tools for comparing two :class:`libbe.bug.BugDir`\s.
+"""
 
 import difflib
 import types
@@ -30,8 +31,7 @@ from libbe.util.utility import time_to_str
 
 
 class SubscriptionType (libbe.util.tree.Tree):
-    """
-    Trees of subscription types to allow users to select exactly what
+    """Trees of subscription types to allow users to select exactly what
     notifications they want to subscribe to.
     """
     def __init__(self, type_name, *args, **kwargs):
@@ -80,7 +80,11 @@ def type_from_name(name, type_root, default=None, default_ok=False):
     raise InvalidType(name, type_root)
 
 class Subscription (object):
-    """
+    """A user subscription.
+
+    Examples
+    --------
+
     >>> subscriptions = [Subscription('XYZ', 'all'),
     ...                  Subscription('DIR', 'new'),
     ...                  Subscription('ABC', BUG_TYPE_ALL),]
@@ -112,7 +116,11 @@ class Subscription (object):
         return '<Subscription: %s (%s)>' % (self.id, self.type)
 
 def subscriptions_from_string(string=None, subscription_sep=',', id_sep=':'):
-    """
+    """Provide a simple way for non-Python interfaces to read in subscriptions.
+
+    Examples
+    --------
+
     >>> subscriptions_from_string(None)
     [<Subscription: DIR (all)>]
     >>> subscriptions_from_string('DIR:new,DIR:rem,ABC:all,XYZ:all')
@@ -135,8 +143,11 @@ def subscriptions_from_string(string=None, subscription_sep=',', id_sep=':'):
     return subscriptions
 
 class DiffTree (libbe.util.tree.Tree):
-    """
-    A tree holding difference data for easy report generation.
+    """A tree holding difference data for easy report generation.
+
+    Examples
+    --------
+
     >>> bugdir = DiffTree('bugdir')
     >>> bdsettings = DiffTree('settings', data='target: None -> 1.0')
     >>> bugdir.append(bdsettings)
@@ -251,8 +262,11 @@ class DiffTree (libbe.util.tree.Tree):
         return data_part
 
 class Diff (object):
-    """
-    Difference tree generator for BugDirs.
+    """Difference tree generator for BugDirs.
+
+    Examples
+    --------
+
     >>> import copy
     >>> bd = libbe.bugdir.SimpleBugDir(memory=True)
     >>> bd_new = copy.deepcopy(bd)
index c3bda4bfdbb57312cfa704a0b297bdd325c84a0c..6bceac93651367a6ea554d74512dfe4fb3b33331 100644 (file)
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
+"""Define the :class:`~libbe.storage.base.Storage` and
+:class:`~libbe.storage.base.VersionedStorage` classes for storing BE
+data.
+
+Also define assorted implementations for the Storage classes:
+
+* :mod:`libbe.storage.vcs`
+* :mod:`libbe.storage.http`
+
+Also define an assortment of storage-related tools and utilities:
+
+* :mod:`libbe.storage.util`
+"""
+
 import base
 
 ConnectionError = base.ConnectionError
index ad6b291402d3340cd2dc5f971831370ab32a82c5..0ae9c53d3a0a63a5ecd451646c3ba859c0384fbe 100644 (file)
@@ -519,10 +519,8 @@ class VersionedStorage (Storage):
         raise InvalidRevision(i)
 
     def changed(self, revision):
-        """
-        Return a tuple of lists of ids
-          (new, modified, removed)
-        from the specified revision to the current situation.
+        """Return a tuple of lists of ids `(new, modified, removed)` from the
+        specified revision to the current situation.
         """
         new = []
         modified = []
index 5606383d19a3f65165c1b944472064c30216d797..7ec9f543713ba0134ad517f290051d3c1cc9f9a9 100644 (file)
 # A dictionary of response codes is available in
 #   httplib.responses
 
-"""
-Access bug repository data over HTTP.
+"""Define an HTTP-based :class:`~libbe.storage.base.VersionedStorage`
+implementation.
+
+See Also
+--------
+:mod:`libbe.command.serve` : the associated server
+
 """
 
 import sys
@@ -50,6 +55,13 @@ HTTP_OK = 200
 HTTP_FOUND = 302
 HTTP_TEMP_REDIRECT = 307
 HTTP_USER_ERROR = 418
+"""Status returned to indicate exceptions on the server side.
+
+A BE-specific extension to the HTTP/1.1 protocol (See `RFC 2616`_).
+
+.. _RFC 2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1
+"""
+
 HTTP_VALID = [HTTP_OK, HTTP_FOUND, HTTP_TEMP_REDIRECT, HTTP_USER_ERROR]
 
 class InvalidURL (Exception):
@@ -66,9 +78,18 @@ class InvalidURL (Exception):
         return self.msg
 
 def get_post_url(url, get=True, data_dict=None, headers=[]):
-    """
-    get:        use GET if True, otherwise use POST.
-    data_dict:  dict of data to send.
+    """Execute a GET or POST transaction.
+
+    Parameters
+    ----------
+    url : str
+      The base URL (query portion added internally, if necessary).
+    get : bool
+      Use GET if True, otherwise use POST.
+    data_dict : dict
+      Data to send, either by URL query (if GET) or by POST (if POST).
+    headers : list
+      Extra HTTP headers to add to the request.
     """
     if data_dict == None:
         data_dict = {}
@@ -101,9 +122,10 @@ def get_post_url(url, get=True, data_dict=None, headers=[]):
 
 
 class HTTP (base.VersionedStorage):
-    """
-    This class implements a Storage interface over HTTP, using GET to
-    retrieve information and POST to set information.
+    """:class:`~libbe.storage.base.VersionedStorage` implementation over
+    HTTP.
+
+    Uses GET to retrieve information and POST to set information.
     """
     name = 'HTTP'
 
@@ -113,6 +135,10 @@ class HTTP (base.VersionedStorage):
 
     def parse_repo(self, repo):
         """Grab username and password (if any) from the repo URL.
+
+        Examples
+        --------
+
         >>> s = HTTP('http://host.com/path/to/repo')
         >>> s.repo
         'http://host.com/path/to/repo'
@@ -249,15 +275,18 @@ class HTTP (base.VersionedStorage):
         return page.rstrip('\n')
 
     def revision_id(self, index=None):
-        """
-        Return the name of the <index>th revision.  The choice of
-        which branch to follow when crossing branches/merges is not
-        defined.  Revision indices start at 1; ID 0 is the blank
-        repository.
+        """Return the name of the <index>th revision.
+
+        The choice of which branch to follow when crossing
+        branches/merges is not defined.  Revision indices start at 1;
+        ID 0 is the blank repository.
 
         Return None if index==None.
 
-        If the specified revision does not exist, raise InvalidRevision.
+        Raises
+        ------
+        InvalidRevision
+          If the specified revision does not exist.
         """
         if index == None:
             return None
index 8526724295041c1a64322bb21f147c2f7ab2f277..724d2d3a17e4b5573da29ef9521f30ad410b88c3 100644 (file)
@@ -16,8 +16,7 @@
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-"""
-Create, save, and load the per-user config file at path().
+"""Create, save, and load the per-user config file at :func:`path`.
 """
 
 import ConfigParser
@@ -31,17 +30,29 @@ if libbe.TESTING == True:
 
 
 default_encoding = libbe.util.encoding.get_filesystem_encoding()
+"""Default filesystem encoding.
+
+Initialized with :func:`libbe.util.encoding.get_filesystem_encoding`.
+"""
 
 def path():
-    """Return the path to the per-user config file"""
+    """Return the path to the per-user config file.
+    """
     return os.path.expanduser("~/.bugs_everywhere")
 
 def set_val(name, value, section="DEFAULT", encoding=None):
-    """Set a value in the per-user config file
+    """Set a value in the per-user config file.
 
-    :param name: The name of the value to set
-    :param value: The new value to set (or None to delete the value)
-    :param section: The section to store the name/value in
+    Parameters
+    ----------
+    name : str
+      The name of the value to set.
+    value : str or None
+      The new value to set (or None to delete the value).
+    section : str
+      The section to store the name/value in.
+    encoding : str
+      The config file's encoding, defaults to :data:`default_encoding`.
     """
     if encoding == None:
         encoding = default_encoding
@@ -60,12 +71,22 @@ def set_val(name, value, section="DEFAULT", encoding=None):
     f.close()
 
 def get_val(name, section="DEFAULT", default=None, encoding=None):
-    """
-    Get a value from the per-user config file
+    """Get a value from the per-user config file
+
+    Parameters
+    ----------
+    name : str
+      The name of the value to set.
+    section : str
+      The section to store the name/value in.
+    default :
+      The value to return if `name` is not set.
+    encoding : str
+      The config file's encoding, defaults to :data:`default_encoding`.
+
+    Examples
+    --------
 
-    :param name: The name of the value to get
-    :section: The section that the name is in
-    :return: The value, or None
     >>> get_val("junk") is None
     True
     >>> set_val("junk", "random")
index 0b8af23c2a206c7c496d8d3b5ea558905a5396f1..55863d7031a024f2cbc9fe6f3bb6b98de005c65c 100644 (file)
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-"""
-Provide a means of saving and loading dictionaries of parameters.  The
-saved "mapfiles" should be clear, flat-text files, and allow easy merging of
-independent/conflicting changes.
+"""Serializing and deserializing dictionaries of parameters.
+
+The serialized "mapfiles" should be clear, flat-text strings, and allow
+easy merging of independent/conflicting changes.
 """
 
 import errno
@@ -49,6 +49,10 @@ class InvalidMapfileContents(Exception):
 
 def generate(map):
     """Generate a YAML mapfile content string.
+
+    Examples
+    --------
+
     >>> generate({'q':'p'})
     'q: p\\n\\n'
     >>> generate({'q':u'Fran\u00e7ais'})
@@ -73,6 +77,10 @@ def generate(map):
     >>> generate({'q':'p\\n'})
     Traceback (most recent call last):
     IllegalValue: Illegal value "p\\n"
+
+    See Also
+    --------
+    parse : inverse
     """
     keys = map.keys()
     keys.sort()
@@ -97,8 +105,11 @@ def generate(map):
     return '\n'.join(lines)
 
 def parse(contents):
-    """
-    Parse a YAML mapfile string.
+    """Parse a YAML mapfile string.
+
+    Examples
+    --------
+
     >>> parse('q: p\\n\\n')['q']
     'p'
     >>> parse('q: \\'p\\'\\n\\n')['q']
@@ -119,6 +130,11 @@ def parse(contents):
     Traceback (most recent call last):
       ...
     InvalidMapfileContents: Invalid YAML contents
+
+    See Also
+    --------
+    generate : inverse
+
     """
     c = yaml.load(contents)
     if type(c) == types.StringType:
index 55bac8520977ec6ef8bfee0ed28cc3d7c9c23c00..b5681b1f199ee1c9905fcee931df8fbeb53194ed 100644 (file)
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-"""
-This module provides a series of useful decorators for defining
-various types of properties.  For example usage, consider the
-unittests at the end of the module.
-
-See
-  http://www.python.org/dev/peps/pep-0318/
-and
-  http://www.phyast.pitt.edu/~micheles/python/documentation.html
-for more information on decorators.
+"""Provides a series of useful decorators for defining various types
+of properties.
+
+For example usage, consider the unittests at the end of the module.
+
+Notes
+-----
+
+See `PEP 318` and Michele Simionato's `decorator documentation` for
+more information on decorators.
+
+.. _PEP 318: http://www.python.org/dev/peps/pep-0318/
+.. _decorator documentation: http://www.phyast.pitt.edu/~micheles/python/documentation.html
+
+See Also
+--------
+:mod:`libbe.storage.util.settings_object` : bundle properties into a convenient package
+
 """
 
 import copy
@@ -336,12 +344,11 @@ def primed_property(primer, initVal=None, unprimeableVal=None):
     return decorator
 
 def change_hook_property(hook, mutable=False, default=None):
-    """
-    Call the function hook(instance, old_value, new_value) whenever a
-    value different from the current value is set (instance is a a
-    reference to the class instance to which this property belongs).
+    """Call the function `hook` whenever a value different from the
+    current value is set.
+
     This is useful for saving changes to disk, etc.  This function is
-    called _after_ the new value has been stored, allowing you to
+    called *after* the new value has been stored, allowing you to
     change the stored value if you want.
 
     In the case of mutables, things are slightly trickier.  Because
@@ -350,11 +357,19 @@ def change_hook_property(hook, mutable=False, default=None):
     mutable value, and checking for changes whenever the property is
     set (obviously) or retrieved (to check for external changes).  So
     long as you're conscientious about accessing the property after
-    making external modifications, mutability won't be a problem.
+    making external modifications, mutability won't be a problem::
+
       t.x.append(5) # external modification
       t.x           # dummy access notices change and triggers hook
-    See testChangeHookMutableProperty for an example of the expected
-    behavior.
+
+    See :class:`testChangeHookMutableProperty` for an example of the
+    expected behavior.
+
+    Parameters
+    ----------
+    hook : fn
+      `hook(instance, old_value, new_value)`, where `instance` is a
+      reference to the class instance to which this property belongs.
     """
     def decorator(funcs):
         if hasattr(funcs, "__call__"):
index 84349520b40ab3e77652df6ad93f7e582ba4f8e9..6e4da55b412a12a092aea3b81fa54baa45bc8dbe 100644 (file)
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-"""
-This module provides a base class implementing settings-dict based
-property storage useful for BE objects with saved properties
-(e.g. BugDir, Bug, Comment).  For example usage, consider the
-unittests at the end of the module.
+"""Provides :class:`SavedSettingsObject` implementing settings-dict
+based property storage.
+
+See Also
+--------
+:mod:`libbe.storage.util.properties` : underlying property definitions
 """
 
 import libbe
@@ -33,9 +34,10 @@ if libbe.TESTING == True:
     import unittest
 
 class _Token (object):
-    """
-    `Control' value class for properties.  We want values that only
-    mean something to the settings_object module.
+    """`Control' value class for properties.
+
+    We want values that only mean something to the `settings_object`
+    module.
     """
     pass
 
@@ -44,45 +46,58 @@ class UNPRIMED (_Token):
     pass
 
 class EMPTY (_Token):
-    """
-    Property has been primed but has no user-set value, so use
+    """Property has been primed but has no user-set value, so use
     default/generator value.
     """
     pass
 
 
 def prop_save_settings(self, old, new):
-    """
-    The default action undertaken when a property changes.
+    """The default action undertaken when a property changes.
     """
     if self.storage != None and self.storage.is_writeable():
         self.save_settings()
 
 def prop_load_settings(self):
-    """
-    The default action undertaken when an UNPRIMED property is
-    accessed.  Attempt to run .load_settings(), which calls
-    ._setup_saved_settings() internally.  If .storage is inaccessible,
-    don't do anything.
+    """The default action undertaken when an UNPRIMED property is
+    accessed.
+
+    Attempt to run `.load_settings()`, which calls
+    `._setup_saved_settings()` internally.  If `.storage` is
+    inaccessible, don't do anything.
     """
     if self.storage != None and self.storage.is_readable():
         self.load_settings()
 
 # Some name-mangling routines for pretty printing setting names
 def setting_name_to_attr_name(self, name):
-    """
-    Convert keys to the .settings dict into their associated
+    """Convert keys to the `.settings` dict into their associated
     SavedSettingsObject attribute names.
+
+    Examples
+    --------
+
     >>> print setting_name_to_attr_name(None,"User-id")
     user_id
+
+    See Also
+    --------
+    attr_name_to_setting_name : inverse
     """
     return name.lower().replace('-', '_')
 
 def attr_name_to_setting_name(self, name):
-    """
-    The inverse of setting_name_to_attr_name.
+    """Convert SavedSettingsObject attribute names to `.settings` dict
+    keys.
+
+    Examples:
+
     >>> print attr_name_to_setting_name(None, "user_id")
     User-id
+
+    See Also
+    --------
+    setting_name_to_attr_name : inverse
     """
     return name.capitalize().replace('_', '-')
 
@@ -96,8 +111,7 @@ def versioned_property(name, doc,
                        settings_properties=[],
                        required_saved_properties=[],
                        require_save=False):
-    """
-    Combine the common decorators in a single function.
+    """Combine the common decorators in a single function.
 
     Use zero or one (but not both) of default or generator, since a
     working default will keep the generator from functioning.  Use the
@@ -124,17 +138,20 @@ def versioned_property(name, doc,
     into our local scope.  Don't mess with them.
 
     Set mutable=True if:
-      * default is a mutable
-      * your generator function may return mutables
-      * you set change_hook and might have mutable property values
-    See the docstrings in libbe.properties for details on how each of
+
+    * default is a mutable
+    * your generator function may return mutables
+    * you set change_hook and might have mutable property values
+
+    See the docstrings in `libbe.properties` for details on how each of
     these cases are handled.
 
-    The value stored in .settings[name] will be
-       * no value (or UNPRIMED) if the property has been neither set,
-         nor loaded as blank.
-       * EMPTY if the value has been loaded as blank.
-       * some value if the property has been either loaded or set.
+    The value stored in `.settings[name]` will be
+
+    * no value (or UNPRIMED) if the property has been neither set,
+      nor loaded as blank.
+    * EMPTY if the value has been loaded as blank.
+    * some value if the property has been either loaded or set.
     """
     settings_properties.append(name)
     if require_save == True:
@@ -175,7 +192,19 @@ def versioned_property(name, doc,
     return decorator
 
 class SavedSettingsObject(object):
-
+    """Setup a framework for lazy saving and loading of `.settings`
+    properties.
+
+    This is useful for BE objects with saved properties
+    (e.g. :class:`~libbe.bugdir.BugDir`, :class:`~libbe.bug.Bug`,
+    :class:`~libbe.comment.Comment`).  For example usage, consider the
+    unittests at the end of the module.
+
+    See Also
+    --------
+    versioned_property, prop_save_settings, prop_load_settings
+    setting_name_to_attr_name, attr_name_to_setting_name
+    """
     # Keep a list of properties that may be stored in the .settings dict.
     #settings_properties = []
 
index 777c72365679443175765863f632b4689e9ddc8a..552d43e3a290bcf86b4bfc96fbc432cc2f05d4bf 100644 (file)
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
+"""Define the Version Controlled System (VCS)-based
+:class:`~libbe.storage.base.Storage` and
+:class:`~libbe.storage.base.VersionedStorage` implementations.
+
+There is a base class (:class:`~libbe.storage.vcs.VCS`) translating 
+Storage language to VCS language, and a number of `VCS` implementations:
+
+* :class:`~libbe.storage.vcs.arch.Arch`
+* :class:`~libbe.storage.vcs.bzr.Bzr`
+* :class:`~libbe.storage.vcs.darcs.Darcs`
+* :class:`~libbe.storage.vcs.git.Git`
+* :class:`~libbe.storage.vcs.hg.Hg`
+
+The base `VCS` class also serves as a filesystem Storage backend (not
+versioning) in the event that a user has no VCS installed.
+"""
+
 import base
 
 set_preferred_vcs = base.set_preferred_vcs
index 38b1d02041a5c9733c9408c6ad3541dc53eb466c..3a50414db40640dff52cd4c5d3c34ddcdeeb40fb 100644 (file)
@@ -18,8 +18,9 @@
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-"""
-GNU Arch (tla) backend.
+"""GNU Arch_ (tla) backend.
+
+.. _Arch: http://www.gnu.org/software/gnu-arch/
 """
 
 import codecs
@@ -56,6 +57,8 @@ def new():
     return Arch()
 
 class Arch(base.VCS):
+    """:class:`base.VCS` implementation for GNU Arch.
+    """
     name = 'arch'
     client = client
     _archive_name = None
@@ -90,10 +93,10 @@ class Arch(base.VCS):
         self._add_project_code(path)
 
     def _create_archive(self, path):
-        """
-        Create a temporary Arch archive in the directory PATH.  This
-        archive will be removed by
-          destroy->_vcs_destroy->_remove_archive
+        """Create a temporary Arch archive in the directory PATH.  This
+        archive will be removed by::
+
+            destroy->_vcs_destroy->_remove_archive
         """
         # http://regexps.srparish.net/tutorial-tla/new-archive.html#Creating_a_New_Archive
         assert self._archive_name == None
@@ -109,8 +112,7 @@ class Arch(base.VCS):
                               self._archive_dir, cwd=path)
 
     def _invoke_client(self, *args, **kwargs):
-        """
-        Invoke the client on our archive.
+        """Invoke the client on our archive.
         """
         assert self._archive_name != None
         command = args[0]
@@ -164,16 +166,20 @@ class Arch(base.VCS):
         return '%s/%s' % (self._archive_name, self._project_name)
 
     def _adjust_naming_conventions(self, path):
-        """
-        By default, Arch restricts source code filenames to
-          ^[_=a-zA-Z0-9].*$
-        See
-          http://regexps.srparish.net/tutorial-tla/naming-conventions.html
-        Since our bug directory '.be' doesn't satisfy these conventions,
-        we need to adjust them.
+        """Adjust `Arch naming conventions`_ so ``.be`` is considered source
+        code.
+
+        By default, Arch restricts source code filenames to::
+
+            ^[_=a-zA-Z0-9].*$
 
-        The conventions are specified in
-          project-root/{arch}/=tagging-method
+        Since our bug directory ``.be`` doesn't satisfy these conventions,
+        we need to adjust them.  The conventions are specified in::
+
+            project-root/{arch}/=tagging-method
+
+        .. _Arch naming conventions:
+          http://regexps.srparish.net/tutorial-tla/naming-conventions.html
         """
         tagpath = os.path.join(path, '{arch}', '=tagging-method')
         lines_out = []
index 337576e242a92eb3b7f777ce1f1727c072f8e075..d85c94d4226ad66d05cb9eca1b100fb9f76e9829 100644 (file)
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-"""
-Define the base VCS (Version Control System) class, which should be
-subclassed by other Version Control System backends.  The base class
-implements a "do not version" VCS.
+"""Define the base :class:`VCS` (Version Control System) class, which
+should be subclassed by other Version Control System backends.  The
+base class implements a "do not version" VCS.
 """
 
 import codecs
@@ -50,11 +49,17 @@ if libbe.TESTING == True:
 
     import libbe.ui.util.user
 
-# List VCS modules in order of preference.
-# Don't list this module, it is implicitly last.
 VCS_ORDER = ['arch', 'bzr', 'darcs', 'git', 'hg']
+"""List VCS modules in order of preference.
+
+Don't list this module, it is implicitly last.
+"""
 
 def set_preferred_vcs(name):
+    """Manipulate :data:`VCS_ORDER` to place `name` first.
+
+    This is primarily indended for testing purposes.
+    """
     global VCS_ORDER
     assert name in VCS_ORDER, \
         'unrecognized VCS %s not in\n  %s' % (name, VCS_ORDER)
@@ -62,7 +67,10 @@ def set_preferred_vcs(name):
     VCS_ORDER.insert(0, name)
 
 def _get_matching_vcs(matchfn):
-    """Return the first module for which matchfn(VCS_instance) is true"""
+    """Return the first module for which matchfn(VCS_instance) is True.
+
+    Searches in :data:`VCS_ORDER`.
+    """
     for submodname in VCS_ORDER:
         module = import_by_name('libbe.storage.vcs.%s' % submodname)
         vcs = module.new()
@@ -71,17 +79,26 @@ def _get_matching_vcs(matchfn):
     return VCS()
 
 def vcs_by_name(vcs_name):
-    """Return the module for the VCS with the given name"""
+    """Return the module for the VCS with the given name.
+
+    Searches in :data:`VCS_ORDER`.
+    """
     if vcs_name == VCS.name:
         return new()
     return _get_matching_vcs(lambda vcs: vcs.name == vcs_name)
 
 def detect_vcs(dir):
-    """Return an VCS instance for the vcs being used in this directory"""
+    """Return an VCS instance for the vcs being used in this directory.
+
+    Searches in :data:`VCS_ORDER`.
+    """
     return _get_matching_vcs(lambda vcs: vcs._detect(dir))
 
 def installed_vcs():
-    """Return an instance of an installed VCS"""
+    """Return an instance of an installed VCS.
+
+    Searches in :data:`VCS_ORDER`.
+    """
     return _get_matching_vcs(lambda vcs: vcs.installed())
 
 
@@ -118,10 +135,17 @@ class NoSuchFile (InvalidID):
 
 
 class CachedPathID (object):
-    """
-    Storage ID <-> path policy.
-      .../.be/BUGDIR/bugs/BUG/comments/COMMENT
-        ^-- root path
+    """Cache Storage ID <-> path policy.
+    Paths generated following::
+
+       .../.be/BUGDIR/bugs/BUG/comments/COMMENT
+          ^-- root path
+
+    See :mod:`libbe.util.id` for a discussion of ID formats.
+
+    Examples
+    --------
 
     >>> dir = Dir()
     >>> os.mkdir(os.path.join(dir.path, '.be'))
@@ -183,10 +207,11 @@ class CachedPathID (object):
             self._root, self._spacer_dirs[0], 'id-cache')
 
     def init(self, verbose=True, cache=None):
-        """
-        Create cache file for an existing .be directory.
-        File if multiple lines of the form:
-          UUID\tPATH
+        """Create cache file for an existing .be directory.
+
+        The file contains multiple lines of the form::
+
+            UUID\tPATH
         """
         if cache == None:
             self._cache = {}
@@ -311,142 +336,13 @@ def new():
     return VCS()
 
 class VCS (libbe.storage.base.VersionedStorage):
-    """
-    This class implements a 'no-vcs' interface.
+    """Implement a 'no-VCS' interface.
 
     Support for other VCSs can be added by subclassing this class, and
-    overriding methods _vcs_*() with code appropriate for your VCS.
+    overriding methods `_vcs_*()` with code appropriate for your VCS.
 
-    The methods _u_*() are utility methods available to the _vcs_*()
+    The methods `_u_*()` are utility methods available to the `_vcs_*()`
     methods.
-
-    Sink to existing root
-    ======================
-
-    Consider the following usage case:
-    You have a bug directory rooted in
-      /path/to/source
-    by which I mean the '.be' directory is at
-      /path/to/source/.be
-    However, you're of in some subdirectory like
-      /path/to/source/GUI/testing
-    and you want to comment on a bug.  Setting sink_to_root=True when
-    you initialize your BugDir will cause it to search for the '.be'
-    file in the ancestors of the path you passed in as 'root'.
-      /path/to/source/GUI/testing/.be     miss
-      /path/to/source/GUI/.be             miss
-      /path/to/source/.be                 hit!
-    So it still roots itself appropriately without much work for you.
-
-    File-system access
-    ==================
-
-    BugDirs live completely in memory when .sync_with_disk is False.
-    This is the default configuration setup by BugDir(from_disk=False).
-    If .sync_with_disk == True (e.g. BugDir(from_disk=True)), then
-    any changes to the BugDir will be immediately written to disk.
-
-    If you want to change .sync_with_disk, we suggest you use
-    .set_sync_with_disk(), which propogates the new setting through to
-    all bugs/comments/etc. that have been loaded into memory.  If
-    you've been living in memory and want to move to
-    .sync_with_disk==True, but you're not sure if anything has been
-    changed in memory, a call to .save() immediately before the
-    .set_sync_with_disk(True) call is a safe move.
-
-    Regardless of .sync_with_disk, a call to .save() will write out
-    all the contents that the BugDir instance has loaded into memory.
-    If sync_with_disk has been True over the course of all interesting
-    changes, this .save() call will be a waste of time.
-
-    The BugDir will only load information from the file system when it
-    loads new settings/bugs/comments that it doesn't already have in
-    memory and .sync_with_disk == True.
-
-    Allow storage initialization
-    ========================
-
-    This one is for testing purposes.  Setting it to True allows the
-    BugDir to search for an installed Storage backend and initialize
-    it in the root directory.  This is a convenience option for
-    supporting tests of versioning functionality
-    (e.g. RevisionedBugDir).
-
-    Disable encoding manipulation
-    =============================
-
-    This one is for testing purposed.  You might have non-ASCII
-    Unicode in your bugs, comments, files, etc.  BugDir instances try
-    and support your preferred encoding scheme (e.g. "utf-8") when
-    dealing with stream and file input/output.  For stream output,
-    this involves replacing sys.stdout and sys.stderr
-    (libbe.encode.set_IO_stream_encodings).  However this messes up
-    doctest's output catching.  In order to support doctest tests
-    using BugDirs, set manipulate_encodings=False, and stick to ASCII
-    in your tests.
-
-        if root == None:
-            root = os.getcwd()
-        if sink_to_existing_root == True:
-            self.root = self._find_root(root)
-        else:
-            if not os.path.exists(root):
-                self.root = None
-                raise NoRootEntry(root)
-            self.root = root
-        # get a temporary storage until we've loaded settings
-        self.sync_with_disk = False
-        self.storage = self._guess_storage()
-
-            if assert_new_BugDir == True:
-                if os.path.exists(self.get_path()):
-                    raise AlreadyInitialized, self.get_path()
-            if storage == None:
-                storage = self._guess_storage(allow_storage_init)
-            self.storage = storage
-            self._setup_user_id(self.user_id)
-
-
-    # methods for getting the BugDir situated in the filesystem
-
-    def _find_root(self, path):
-        '''
-        Search for an existing bug database dir and it's ancestors and
-        return a BugDir rooted there.  Only called by __init__, and
-        then only if sink_to_existing_root == True.
-        '''
-        if not os.path.exists(path):
-            self.root = None
-            raise NoRootEntry(path)
-        versionfile=utility.search_parent_directories(path,
-                                                      os.path.join(".be", "version"))
-        if versionfile != None:
-            beroot = os.path.dirname(versionfile)
-            root = os.path.dirname(beroot)
-            return root
-        else:
-            beroot = utility.search_parent_directories(path, ".be")
-            if beroot == None:
-                self.root = None
-                raise NoBugDir(path)
-            return beroot
-
-    def _guess_storage(self, allow_storage_init=False):
-        '''
-        Only called by __init__.
-        '''
-        deepdir = self.get_path()
-        if not os.path.exists(deepdir):
-            deepdir = os.path.dirname(deepdir)
-        new_storage = storage.detect_storage(deepdir)
-        install = False
-        if new_storage.name == "None":
-            if allow_storage_init == True:
-                new_storage = storage.installed_storage()
-                new_storage.init(self.root)
-        return new_storage
-
-os.listdir(self.get_path("bugs")):
     """
     name = 'None'
     client = 'false' # command-line tool for _u_invoke_client
@@ -659,9 +555,28 @@ os.listdir(self.get_path("bugs")):
         return self._vcs_detect(path)
 
     def root(self):
-        """
-        Set the root directory to the path's VCS root.  This is the
-        default working directory for future invocations.
+        """Set the root directory to the path's VCS root.
+
+        This is the default working directory for future invocations.
+        Consider the following usage case:
+
+        You have a project rooted in::
+
+          /path/to/source/
+
+        by which I mean the VCS repository is in, for example::
+
+          /path/to/source/.bzr
+
+        However, you're of in some subdirectory like::
+
+          /path/to/source/ui/testing
+
+        and you want to comment on a bug.  `root` will locate your VCS
+        root (``/path/to/source/``) and set the repo there.  This
+        means that it doesn't matter where you are in your project
+        tree when you call "be COMMAND", it always acts as if you called
+        it from the VCS root.
         """
         if self._detect(self.repo) == False:
             raise VCSUnableToRoot(self)
@@ -678,6 +593,10 @@ os.listdir(self.get_path("bugs")):
         """
         Begin versioning the tree based at self.repo.
         Also roots the vcs at path.
+
+        See Also
+        --------
+        root : called if the VCS has already been initialized.
         """
         if not os.path.exists(self.repo) or not os.path.isdir(self.repo):
             raise VCSUnableToRoot(self)
@@ -908,8 +827,7 @@ os.listdir(self.get_path("bugs")):
         return (new_id, mod_id, rem_id)
 
     def _u_any_in_string(self, list, string):
-        """
-        Return True if any of the strings in list are in string.
+        """Return True if any of the strings in list are in string.
         Otherwise return False.
         """
         for list_string in list:
@@ -932,9 +850,8 @@ os.listdir(self.get_path("bugs")):
         return self._u_invoke(cl_args, **kwargs)
 
     def _u_search_parent_directories(self, path, filename):
-        """
-        Find the file (or directory) named filename in path or in any
-        of path's parents.
+        """Find the file (or directory) named filename in path or in any of
+        path's parents.
 
         e.g.
           search_parent_directories("/a/b/c", ".be")
@@ -952,8 +869,8 @@ os.listdir(self.get_path("bugs")):
         return ret
 
     def _u_find_id_from_manifest(self, id, manifest, revision=None):
-        """
-        Search for the relative path to id using manifest, a list of all files.
+        """Search for the relative path to id using manifest, a list of all
+        files.
         
         Returns None if the id is not found.
         """
@@ -979,8 +896,8 @@ os.listdir(self.get_path("bugs")):
         raise InvalidID(id, revision=revision)
 
     def _u_find_id(self, id, revision):
-        """
-        Search for the relative path to id as of revision.
+        """Search for the relative path to id as of revision.
+
         Returns None if the id is not found.
         """
         assert self._rooted == True
@@ -1001,8 +918,10 @@ os.listdir(self.get_path("bugs")):
         return self._cached_path_id.id(path)
 
     def _u_rel_path(self, path, root=None):
-        """
-        Return the relative path to path from root.
+        """Return the relative path to path from root.
+
+        Examples:
+
         >>> vcs = new()
         >>> vcs._u_rel_path("/a.b/c/.be", "/a.b/c")
         '.be'
@@ -1028,8 +947,11 @@ os.listdir(self.get_path("bugs")):
         return relpath
 
     def _u_abspath(self, path, root=None):
-        """
-        Return the absolute path from a path realtive to root.
+        """Return the absolute path from a path realtive to root.
+
+        Examples
+        --------
+
         >>> vcs = new()
         >>> vcs._u_abspath(".be", "/a.b/c")
         '/a.b/c/.be'
@@ -1040,9 +962,8 @@ os.listdir(self.get_path("bugs")):
         return os.path.abspath(os.path.join(root, path))
 
     def _u_parse_commitfile(self, commitfile):
-        """
-        Split the commitfile created in self.commit() back into
-        summary and header lines.
+        """Split the commitfile created in self.commit() back into summary and
+        header lines.
         """
         f = codecs.open(commitfile, 'r', self.encoding)
         summary = f.readline()
@@ -1059,8 +980,11 @@ os.listdir(self.get_path("bugs")):
             upgrade.upgrade(self.repo, version)
 
     def storage_version(self, revision=None, path=None):
-        """
-        Requires disk access.
+        """Return the storage version of the on-disk files.
+
+        See Also
+        --------
+        :mod:`libbe.storage.util.upgrade`
         """
         if path == None:
             path = os.path.join(self.repo, '.be', 'version')
index 01d99488a5d296efbc8ebe0387820ef2b00fdca6..5a6296803a28dcc3701f831fc61f291185d170e7 100644 (file)
@@ -18,8 +18,9 @@
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-"""
-Bazaar (bzr) backend.
+"""Bazaar_ (bzr) backend.
+
+.. _Bazaar: http://bazaar.canonical.com/
 """
 
 try:
@@ -51,6 +52,8 @@ def new():
     return Bzr()
 
 class Bzr(base.VCS):
+    """:class:`base.VCS` implementation for Bazaar.
+    """
     name = 'bzr'
     client = None # bzrlib module
 
@@ -64,12 +67,18 @@ class Bzr(base.VCS):
         return bzrlib.__version__
 
     def version_cmp(self, *args):
-        """
-        Compare the installed Bazaar version V_i with another version
-        V_o (given in *args).  Returns
-           1 if V_i > V_o,
-           0 if V_i == V_o, and
-          -1 if V_i < V_o
+        """Compare the installed Bazaar version `V_i` with another version
+        `V_o` (given in `*args`).  Returns
+
+           === ===============
+            1  if `V_i > V_o`
+            0  if `V_i == V_o`
+           -1  if `V_i < V_o`
+           === ===============
+
+        Examples
+        --------
+
         >>> b = Bzr(repo='.')
         >>> b._vcs_version = lambda : "2.3.1 (release)"
         >>> b.version_cmp(2,3,1)
@@ -275,51 +284,54 @@ class Bzr(base.VCS):
         return cmd.outf.getvalue()
 
     def _parse_diff(self, diff_text):
-        """
-        Example diff text:
-                
-        === modified file 'dir/changed'
-        --- dir/changed        2010-01-16 01:54:53 +0000
-        +++ dir/changed        2010-01-16 01:54:54 +0000
-        @@ -1,3 +1,3 @@
-         hi
-        -there
-        +everyone and
-         joe
-        
-        === removed file 'dir/deleted'
-        --- dir/deleted        2010-01-16 01:54:53 +0000
-        +++ dir/deleted        1970-01-01 00:00:00 +0000
-        @@ -1,3 +0,0 @@
-        -in
-        -the
-        -beginning
-        
-        === removed file 'dir/moved'
-        --- dir/moved  2010-01-16 01:54:53 +0000
-        +++ dir/moved  1970-01-01 00:00:00 +0000
-        @@ -1,4 +0,0 @@
-        -the
-        -ants
-        -go
-        -marching
-        
-        === added file 'dir/moved2'
-        --- dir/moved2 1970-01-01 00:00:00 +0000
-        +++ dir/moved2 2010-01-16 01:54:34 +0000
-        @@ -0,0 +1,4 @@
-        +the
-        +ants
-        +go
-        +marching
-        
-        === added file 'dir/new'
-        --- dir/new    1970-01-01 00:00:00 +0000
-        +++ dir/new    2010-01-16 01:54:54 +0000
-        @@ -0,0 +1,2 @@
-        +hello
-        +world
-        
+        """_parse_diff(diff_text) -> (new,modified,removed)
+
+        `new`, `modified`, and `removed` are lists of files.
+
+        Example diff text::
+
+          === modified file 'dir/changed'
+          --- dir/changed      2010-01-16 01:54:53 +0000
+          +++ dir/changed      2010-01-16 01:54:54 +0000
+          @@ -1,3 +1,3 @@
+           hi
+          -there
+          +everyone and
+           joe
+          
+          === removed file 'dir/deleted'
+          --- dir/deleted      2010-01-16 01:54:53 +0000
+          +++ dir/deleted      1970-01-01 00:00:00 +0000
+          @@ -1,3 +0,0 @@
+          -in
+          -the
+          -beginning
+          
+          === removed file 'dir/moved'
+          --- dir/moved        2010-01-16 01:54:53 +0000
+          +++ dir/moved        1970-01-01 00:00:00 +0000
+          @@ -1,4 +0,0 @@
+          -the
+          -ants
+          -go
+          -marching
+          
+          === added file 'dir/moved2'
+          --- dir/moved2       1970-01-01 00:00:00 +0000
+          +++ dir/moved2       2010-01-16 01:54:34 +0000
+          @@ -0,0 +1,4 @@
+          +the
+          +ants
+          +go
+          +marching
+          
+          === added file 'dir/new'
+          --- dir/new  1970-01-01 00:00:00 +0000
+          +++ dir/new  2010-01-16 01:54:54 +0000
+          @@ -0,0 +1,2 @@
+          +hello
+          +world
+          
         """
         new = []
         modified = []
index fd8b7d5c11377fe77c1604c552f37b02ba1f7a1a..4a218884d3d0530afc2e9334a79e738ca3090004 100644 (file)
@@ -15,8 +15,9 @@
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-"""
-Darcs backend.
+"""Darcs_ backend.
+
+.. _Darcs: http://darcs.net/
 """
 
 import codecs
@@ -44,6 +45,8 @@ def new():
     return Darcs()
 
 class Darcs(base.VCS):
+    """:class:`base.VCS` implementation for Darcs.
+    """
     name='darcs'
     client='darcs'
 
@@ -57,12 +60,18 @@ class Darcs(base.VCS):
         return output.strip()
 
     def version_cmp(self, *args):
-        """
-        Compare the installed darcs version V_i with another version
-        V_o (given in *args).  Returns
-           1 if V_i > V_o,
-           0 if V_i == V_o, and
-          -1 if V_i < V_o
+        """Compare the installed Darcs version `V_i` with another version
+        `V_o` (given in `*args`).  Returns
+
+           === ===============
+            1  if `V_i > V_o`
+            0  if `V_i == V_o`
+           -1  if `V_i < V_o`
+           === ===============
+
+        Examples
+        --------
+
         >>> d = Darcs(repo='.')
         >>> d._vcs_version = lambda : "2.3.1 (release)"
         >>> d.version_cmp(2,3,1)
@@ -295,44 +304,47 @@ class Darcs(base.VCS):
         return output
 
     def _parse_diff(self, diff_text):
-        """
-        Example diff text:
-
-        Mon Jan 18 15:19:30 EST 2010  None <None@invalid.com>
-          * Final state
-        diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/modified new-BEtestgQtDuD/.be/dir/bugs/modified
-        --- old-BEtestgQtDuD/.be/dir/bugs/modified      2010-01-18 15:19:30.000000000 -0500
-        +++ new-BEtestgQtDuD/.be/dir/bugs/modified      2010-01-18 15:19:30.000000000 -0500
-        @@ -1 +1 @@
-        -some value to be modified
-        \ No newline at end of file
-        +a new value
-        \ No newline at end of file
-        diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/moved new-BEtestgQtDuD/.be/dir/bugs/moved
-        --- old-BEtestgQtDuD/.be/dir/bugs/moved 2010-01-18 15:19:30.000000000 -0500
-        +++ new-BEtestgQtDuD/.be/dir/bugs/moved 1969-12-31 19:00:00.000000000 -0500
-        @@ -1 +0,0 @@
-        -this entry will be moved
-        \ No newline at end of file
-        diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/moved2 new-BEtestgQtDuD/.be/dir/bugs/moved2
-        --- old-BEtestgQtDuD/.be/dir/bugs/moved2        1969-12-31 19:00:00.000000000 -0500
-        +++ new-BEtestgQtDuD/.be/dir/bugs/moved2        2010-01-18 15:19:30.000000000 -0500
-        @@ -0,0 +1 @@
-        +this entry will be moved
-        \ No newline at end of file
-        diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/new new-BEtestgQtDuD/.be/dir/bugs/new
-        --- old-BEtestgQtDuD/.be/dir/bugs/new   1969-12-31 19:00:00.000000000 -0500
-        +++ new-BEtestgQtDuD/.be/dir/bugs/new   2010-01-18 15:19:30.000000000 -0500
-        @@ -0,0 +1 @@
-        +this entry is new
-        \ No newline at end of file
-        diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/removed new-BEtestgQtDuD/.be/dir/bugs/removed
-        --- old-BEtestgQtDuD/.be/dir/bugs/removed       2010-01-18 15:19:30.000000000 -0500
-        +++ new-BEtestgQtDuD/.be/dir/bugs/removed       1969-12-31 19:00:00.000000000 -0500
-        @@ -1 +0,0 @@
-        -this entry will be deleted
-        \ No newline at end of file
-        
+        """_parse_diff(diff_text) -> (new,modified,removed)
+
+        `new`, `modified`, and `removed` are lists of files.
+
+        Example diff text::
+
+          Mon Jan 18 15:19:30 EST 2010  None <None@invalid.com>
+            * Final state
+          diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/modified new-BEtestgQtDuD/.be/dir/bugs/modified
+          --- old-BEtestgQtDuD/.be/dir/bugs/modified      2010-01-18 15:19:30.000000000 -0500
+          +++ new-BEtestgQtDuD/.be/dir/bugs/modified      2010-01-18 15:19:30.000000000 -0500
+          @@ -1 +1 @@
+          -some value to be modified
+          \ No newline at end of file
+          +a new value
+          \ No newline at end of file
+          diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/moved new-BEtestgQtDuD/.be/dir/bugs/moved
+          --- old-BEtestgQtDuD/.be/dir/bugs/moved 2010-01-18 15:19:30.000000000 -0500
+          +++ new-BEtestgQtDuD/.be/dir/bugs/moved 1969-12-31 19:00:00.000000000 -0500
+          @@ -1 +0,0 @@
+          -this entry will be moved
+          \ No newline at end of file
+          diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/moved2 new-BEtestgQtDuD/.be/dir/bugs/moved2
+          --- old-BEtestgQtDuD/.be/dir/bugs/moved2        1969-12-31 19:00:00.000000000 -0500
+          +++ new-BEtestgQtDuD/.be/dir/bugs/moved2        2010-01-18 15:19:30.000000000 -0500
+          @@ -0,0 +1 @@
+          +this entry will be moved
+          \ No newline at end of file
+          diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/new new-BEtestgQtDuD/.be/dir/bugs/new
+          --- old-BEtestgQtDuD/.be/dir/bugs/new   1969-12-31 19:00:00.000000000 -0500
+          +++ new-BEtestgQtDuD/.be/dir/bugs/new   2010-01-18 15:19:30.000000000 -0500
+          @@ -0,0 +1 @@
+          +this entry is new
+          \ No newline at end of file
+          diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/removed new-BEtestgQtDuD/.be/dir/bugs/removed
+          --- old-BEtestgQtDuD/.be/dir/bugs/removed       2010-01-18 15:19:30.000000000 -0500
+          +++ new-BEtestgQtDuD/.be/dir/bugs/removed       1969-12-31 19:00:00.000000000 -0500
+          @@ -1 +0,0 @@
+          -this entry will be deleted
+          \ No newline at end of file
+          
         """
         new = []
         modified = []
index c6638bc5f3622bf762e9fdaa1debddfcd4e6ad62..4df9bc865ba97c843616572f2149c11304f7850f 100644 (file)
@@ -17,8 +17,9 @@
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-"""
-Git backend.
+"""Git_ backend.
+
+.. _Git: http://git-scm.com/
 """
 
 import os
@@ -40,6 +41,8 @@ def new():
     return Git()
 
 class Git(base.VCS):
+    """:class:`base.VCS` implementation for Git.
+    """
     name='git'
     client='git'
 
@@ -179,55 +182,58 @@ class Git(base.VCS):
         return output
 
     def _parse_diff(self, diff_text):
-        """
-        Example diff text:
-        
-        diff --git a/dir/changed b/dir/changed
-        index 6c3ea8c..2f2f7c7 100644
-        --- a/dir/changed
-        +++ b/dir/changed
-        @@ -1,3 +1,3 @@
-         hi
-        -there
-        +everyone and
-         joe
-        diff --git a/dir/deleted b/dir/deleted
-        deleted file mode 100644
-        index 225ec04..0000000
-        --- a/dir/deleted
-        +++ /dev/null
-        @@ -1,3 +0,0 @@
-        -in
-        -the
-        -beginning
-        diff --git a/dir/moved b/dir/moved
-        deleted file mode 100644
-        index 5ef102f..0000000
-        --- a/dir/moved
-        +++ /dev/null
-        @@ -1,4 +0,0 @@
-        -the
-        -ants
-        -go
-        -marching
-        diff --git a/dir/moved2 b/dir/moved2
-        new file mode 100644
-        index 0000000..5ef102f
-        --- /dev/null
-        +++ b/dir/moved2
-        @@ -0,0 +1,4 @@
-        +the
-        +ants
-        +go
-        +marching
-        diff --git a/dir/new b/dir/new
-        new file mode 100644
-        index 0000000..94954ab
-        --- /dev/null
-        +++ b/dir/new
-        @@ -0,0 +1,2 @@
-        +hello
-        +world
+        """_parse_diff(diff_text) -> (new,modified,removed)
+
+        `new`, `modified`, and `removed` are lists of files.
+
+        Example diff text::
+
+          diff --git a/dir/changed b/dir/changed
+          index 6c3ea8c..2f2f7c7 100644
+          --- a/dir/changed
+          +++ b/dir/changed
+          @@ -1,3 +1,3 @@
+           hi
+          -there
+          +everyone and
+           joe
+          diff --git a/dir/deleted b/dir/deleted
+          deleted file mode 100644
+          index 225ec04..0000000
+          --- a/dir/deleted
+          +++ /dev/null
+          @@ -1,3 +0,0 @@
+          -in
+          -the
+          -beginning
+          diff --git a/dir/moved b/dir/moved
+          deleted file mode 100644
+          index 5ef102f..0000000
+          --- a/dir/moved
+          +++ /dev/null
+          @@ -1,4 +0,0 @@
+          -the
+          -ants
+          -go
+          -marching
+          diff --git a/dir/moved2 b/dir/moved2
+          new file mode 100644
+          index 0000000..5ef102f
+          --- /dev/null
+          +++ b/dir/moved2
+          @@ -0,0 +1,4 @@
+          +the
+          +ants
+          +go
+          +marching
+          diff --git a/dir/new b/dir/new
+          new file mode 100644
+          index 0000000..94954ab
+          --- /dev/null
+          +++ b/dir/new
+          @@ -0,0 +1,2 @@
+          +hello
+          +world
         """
         new = []
         modified = []
index 97fc470df2f34e71a1758a0c17140299bf14c0d3..93783367c728e162e58f485569784749a3a0c7d0 100644 (file)
@@ -17,8 +17,9 @@
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-"""
-Mercurial (hg) backend.
+"""Mercurial_ (hg) backend.
+
+.. _Mercurial: http://mercurial.selenic.com/
 """
 
 try:
@@ -58,6 +59,8 @@ def new():
     return Hg()
 
 class Hg(base.VCS):
+    """:class:`base.VCS` implementation for Mercurial.
+    """
     name='hg'
     client=None # mercurial module
 
@@ -177,45 +180,48 @@ class Hg(base.VCS):
             'diff', '-r', revision, '--git')
 
     def _parse_diff(self, diff_text):
-        """
-        Example diff text:
+        """_parse_diff(diff_text) -> (new,modified,removed)
+
+        `new`, `modified`, and `removed` are lists of files.
+
+        Example diff text::
                 
-        diff --git a/.be/dir/bugs/modified b/.be/dir/bugs/modified
-        --- a/.be/dir/bugs/modified
-        +++ b/.be/dir/bugs/modified
-        @@ -1,1 +1,1 @@ some value to be modified
-        -some value to be modified
-        \ No newline at end of file
-        +a new value
-        \ No newline at end of file
-        diff --git a/.be/dir/bugs/moved b/.be/dir/bugs/moved
-        deleted file mode 100644
-        --- a/.be/dir/bugs/moved
-        +++ /dev/null
-        @@ -1,1 +0,0 @@
-        -this entry will be moved
-        \ No newline at end of file
-        diff --git a/.be/dir/bugs/moved2 b/.be/dir/bugs/moved2
-        new file mode 100644
-        --- /dev/null
-        +++ b/.be/dir/bugs/moved2
-        @@ -0,0 +1,1 @@
-        +this entry will be moved
-        \ No newline at end of file
-        diff --git a/.be/dir/bugs/new b/.be/dir/bugs/new
-        new file mode 100644
-        --- /dev/null
-        +++ b/.be/dir/bugs/new
-        @@ -0,0 +1,1 @@
-        +this entry is new
-        \ No newline at end of file
-        diff --git a/.be/dir/bugs/removed b/.be/dir/bugs/removed
-        deleted file mode 100644
-        --- a/.be/dir/bugs/removed
-        +++ /dev/null
-        @@ -1,1 +0,0 @@
-        -this entry will be deleted
-        \ No newline at end of file
+          diff --git a/.be/dir/bugs/modified b/.be/dir/bugs/modified
+          --- a/.be/dir/bugs/modified
+          +++ b/.be/dir/bugs/modified
+          @@ -1,1 +1,1 @@ some value to be modified
+          -some value to be modified
+          \ No newline at end of file
+          +a new value
+          \ No newline at end of file
+          diff --git a/.be/dir/bugs/moved b/.be/dir/bugs/moved
+          deleted file mode 100644
+          --- a/.be/dir/bugs/moved
+          +++ /dev/null
+          @@ -1,1 +0,0 @@
+          -this entry will be moved
+          \ No newline at end of file
+          diff --git a/.be/dir/bugs/moved2 b/.be/dir/bugs/moved2
+          new file mode 100644
+          --- /dev/null
+          +++ b/.be/dir/bugs/moved2
+          @@ -0,0 +1,1 @@
+          +this entry will be moved
+          \ No newline at end of file
+          diff --git a/.be/dir/bugs/new b/.be/dir/bugs/new
+          new file mode 100644
+          --- /dev/null
+          +++ b/.be/dir/bugs/new
+          @@ -0,0 +1,1 @@
+          +this entry is new
+          \ No newline at end of file
+          diff --git a/.be/dir/bugs/removed b/.be/dir/bugs/removed
+          deleted file mode 100644
+          --- a/.be/dir/bugs/removed
+          +++ /dev/null
+          @@ -1,1 +0,0 @@
+          -this entry will be deleted
+          \ No newline at end of file
         """
         new = []
         modified = []
index 81f5396f124d857c2b17e0e9c034b665037e47d6..76079e7fa90c884ad90ad96d51775852a62155da 100644 (file)
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-"""
-Handle ID creation and parsing.
+"""Handle ID creation and parsing.
+
+Format
+======
+
+BE IDs are formatted::
+
+    <bug-directory>[/<bug>[/<comment>]]
+
+where each ``<..>`` is a UUID.  For example::
+
+    bea86499-824e-4e77-b085-2d581fa9ccab/3438b72c-6244-4f1d-8722-8c8d41484e35
+
+refers to bug ``3438b72c-6244-4f1d-8722-8c8d41484e35`` which is
+located in bug directory ``bea86499-824e-4e77-b085-2d581fa9ccab``.
+This is a bit of a mouthful, so you can truncate each UUID so long as
+it remains unique.  For example::
+
+    bea/343
+
+If there were two bugs ``3438...`` and ``343a...`` in ``bea``, you'd
+have to use::
+
+    bea/3438
+
+BE will only truncate each UUID down to three characters to slightly
+future-proof the short user ids.  However, if you want to save keystrokes
+and you *know* there is only one bug directory, feel free to truncate
+all the way to zero characters::
+
+    /3438
+
+Cross references
+================
+
+To refer to other bug-directories/bugs/comments from bug comments, simply
+enclose the ID in pound signs (``#``).  BE will automatically expand the
+truncations to the full UUIDs before storing the comment, and the reference
+will be appropriately truncated (and hyperlinked, if possible) when the
+comment is displayed.
+
+Scope
+=====
+
+Although bug and comment IDs always appear in compound references,
+UUIDs at each level are globally unique.  For example, comment
+``bea/343/ba96f1c0-ba48-4df8-aaf0-4e3a3144fc46`` will *only* appear
+under ``bea/343``.  The prefix (``bea/343``) allows BE to reduce
+caching global comment-lookup tables and enables easy error messages
+("I couldn't find ``bea/343/ba9`` because I don't know where the
+``bea`` bug directory is located").
 """
 
 import os.path
@@ -64,9 +113,21 @@ except ImportError:
 
 
 HIERARCHY = ['bugdir', 'bug', 'comment']
-
+"""Keep track of the object type hierarchy.
+"""
 
 class MultipleIDMatches (ValueError):
+    """Multiple IDs match the given user ID.
+
+    Parameters
+    ----------
+    id : str
+      The not-specific-enough truncated UUID.
+    common : str
+      The initial characters common to all matching UUIDs.
+    matches : list of str
+      The list of possibly matching UUIDs.
+    """
     def __init__(self, id, common, matches):
         msg = ('More than one id matches %s.  '
                'Please be more specific (%s*).\n%s' % (id, common, matches))
@@ -76,6 +137,17 @@ class MultipleIDMatches (ValueError):
         self.matches = matches
 
 class NoIDMatches (KeyError):
+    """No IDs match the given user ID.
+
+    Parameters
+    ----------
+    id : str
+      The not-matching, possibly truncated UUID.
+    possible_ids : list of str
+      The list of potential UUIDs at that level.
+    msg : str, optional
+      A helpful message explaining what went wrong.
+    """
     def __init__(self, id, possible_ids, msg=None):
         KeyError.__init__(self, id)
         self.id = id
@@ -87,6 +159,15 @@ class NoIDMatches (KeyError):
         return self.msg
 
 class InvalidIDStructure (KeyError):
+    """A purported ID does not have the appropriate syntax.
+
+    Parameters
+    ----------
+    id : str
+      The purported ID.
+    msg : str, optional
+      A helpful message explaining what went wrong.
+    """
     def __init__(self, id, msg=None):
         KeyError.__init__(self, id)
         self.id = id
@@ -97,6 +178,12 @@ class InvalidIDStructure (KeyError):
         return self.msg
 
 def _assemble(args, check_length=False):
+    """Join a bunch of level UUIDs into a single ID.
+
+    See Also
+    --------
+    _split : inverse
+    """
     args = list(args)
     for i,arg in enumerate(args):
         if arg == None:
@@ -104,22 +191,47 @@ def _assemble(args, check_length=False):
     id = '/'.join(args)
     if check_length == True:
         assert len(args) > 0, args
-        if len(args) > 3:
-            raise InvalidIDStructure(id, '%d > 3 levels in "%s"' % (len(args), id))
+        if len(args) > len(HIERARCHY):
+            raise InvalidIDStructure(
+                id, '%d > %d levels in "%s"' % (len(args), len(HIERARCHY), id))
     return id
 
 def _split(id, check_length=False):
+    """Split an ID into a list of level UUIDs.
+
+    See Also
+    --------
+    _assemble : inverse
+    """
     args = id.split('/')
     for i,arg in enumerate(args):
         if arg == '':
             args[i] = None
     if check_length == True:
         assert len(args) > 0, args
-        if len(args) > 3:
-            raise InvalidIDStructure(id, '%d > 3 levels in "%s"' % (len(args), id))
+        if len(args) > len(HIERARCHY):
+            raise InvalidIDStructure(
+                id, '%d > %d levels in "%s"' % (len(args), len(HIERARCHY), id))
     return args
 
 def _truncate(uuid, other_uuids, min_length=3):
+    """Truncate a UUID to the shortest length >= `min_length` such that it
+    is *not* a truncated form of a UUID in `other_uuids`.
+
+    Parameters
+    ----------
+    uuid : str
+      The UUID to truncate.
+    other_uuids : list of str
+      The other UUIDs which the truncation *might* (but doesn't) refer
+      to.
+    min_length : int
+      Avoid rapidly outdated truncations, even if they are unique now.
+
+    See Also
+    --------
+    _expand : inverse
+    """
     chars = min_length
     for id in other_uuids:
         if id == uuid:
@@ -129,6 +241,29 @@ def _truncate(uuid, other_uuids, min_length=3):
     return uuid[:chars]
 
 def _expand(truncated_id, common, other_ids):
+    """Expand a truncated UUID.
+
+    Parameters
+    ----------
+    truncated_id : str
+      The ID to expand.
+    common : str
+      The common portion `truncated_id` shares with the UUIDs in
+      `other_ids`.  Not used by ``_expand``, but passed on to the
+      matching exceptions if they occur.
+    other_uuids : list of str
+      The other UUIDs which the truncation *might* (but doesn't) refer
+      to.
+
+    Raises
+    ------
+    NoIDMatches
+    MultipleIDMatches
+
+    See Also
+    --------
+    _expand : inverse
+    """
     other_ids = list(other_ids)
     if len(other_ids) == 0:
         raise NoIDMatches(truncated_id, other_ids)
@@ -151,7 +286,18 @@ def _expand(truncated_id, common, other_ids):
 
 
 class ID (object):
-    """
+    """Store an object ID and produce various representations.
+
+    Parameters
+    ----------
+    object : :class:`~libbe.bugdir.BugDir` or :class:`~libbe.bug.Bug` or :class:`~libbe.comment.Comment`
+      The object that the ID applies to.
+    type : 'bugdir' or 'bug' or 'comment'
+      The type of the object.
+
+    Notes
+    -----
+
     IDs have several formats specialized for different uses.
 
     In storage, all objects are represented by their uuid alone,
@@ -166,41 +312,39 @@ class ID (object):
     them while retaining local uniqueness (with regards to the other
     objects currently in storage).  We also prepend truncated parent
     ids for two reasons:
-      (1) so that a user can locate the repository containing the
-          referenced object.  It would be hard to find bug 'XYZ' if
-          that's all you knew.  Much easier with 'ABC/XYZ', where ABC
-          is the bugdir.  Each project can publish a list of bugdir-id
-          - to - location mappings, e.g.
+
+    1. So that a user can locate the repository containing the
+       referenced object.  It would be hard to find bug ``XYZ`` if
+       that's all you knew.  Much easier with ``ABC/XYZ``, where
+       ``ABC`` is the bugdir.  Each project can publish a list of
+       bugdir-id-to-location mappings, e.g.::
+
             ABC...(full uuid)...DEF   https://server.com/projectX/be/
-          which is easier than publishing all-object-ids-to-location
-          mappings.
-      (2) because it's easier to generate and parse truncated ids if
-          you don't have to fetch all the ids in the storage
-          repository, but can restrict yourself to a specific branch.
-    You can generate ids of this sort with the .user() method,
-    although in order to preform the truncation, your object (and its
-    parents must define a .sibling_uuids() method.
 
+       which is easier than publishing all-object-ids-to-location
+       mappings.
+
+    2. Because it's easier to generate and parse truncated ids if you
+       don't have to fetch all the ids in the storage repository but
+       can restrict yourself to a specific branch.
+
+    You can generate ids of this sort with the :meth:`user` method,
+    although in order to preform the truncation, your object (and its
+    parents must define a `sibling_uuids` method.
 
     While users can use the convenient short user ids in the short
     term, the truncation will inevitably lead to name collision.  To
     avoid that, we provide a non-truncated form of the short user ids
-    via the .long_user() method.  These long user ids should be
+    via the :meth:`long_user` method.  These long user ids should be
     converted to short user ids by intelligent user interfaces.
 
-    Related tools:
-      * get uuids back out of the user ids:
-        parse_user()
-      * convert a single short user id to a long user id:
-        short_to_long_user()
-      * convert a single long user id to a short user id:
-        long_to_short_user()
-      * scan text for user ids & convert to long user ids:
-        short_to_long_text()
-      * scan text for long user ids & convert to short user ids:
-        long_to_short_text()
-
-    Supported types: 'bugdir', 'bug', 'comment'
+    See Also
+    --------
+    parse_user : get uuids back out of the user ids.
+    short_to_long_user : convert a single short user id to a long user id.
+    long_to_short_user : convert a single long user id to a short user id.
+    short_to_long_text : scan text for user ids & convert to long user ids.
+    long_to_short_text : scan text for long user ids & convert to short user ids.
     """
     def __init__(self, object, type):
         self._object = object
@@ -236,9 +380,17 @@ class ID (object):
         return _assemble(ids, check_length=True)
 
 def child_uuids(child_storage_ids):
-    """
-    Extract uuid children from other children generated by the
-    ID.storage() method.
+    """Extract uuid children from other children generated by
+    :meth:`ID.storage`.
+
+    This is useful for separating data belonging to a particular
+    object directly from entries for its child objects.  Since the
+    :class:`~libbe.storage.base.Storage` backend doesn't distinguish
+    between the two.
+
+    Examples
+    --------
+
     >>> list(child_uuids(['abc123/values', '123abc', '123def']))
     ['123abc', '123def']
     """
@@ -248,6 +400,15 @@ def child_uuids(child_storage_ids):
             yield fields[0]
 
 def long_to_short_user(bugdirs, id):
+    """Convert a long user ID to a short user ID (see :class:`ID`).
+    The list of bugdirs allows uniqueness-maintaining truncation of
+    the bugdir portion of the ID.
+
+    See Also
+    --------
+    short_to_long_user : inverse
+    long_to_short_text : conversion on a block of text
+    """
     ids = _split(id, check_length=True)
     matching_bugdirs = [bd for bd in bugdirs if bd.uuid == ids[0]]
     if len(matching_bugdirs) == 0:
@@ -267,6 +428,15 @@ def long_to_short_user(bugdirs, id):
     return _assemble(ids)
 
 def short_to_long_user(bugdirs, id):
+    """Convert a short user ID to a long user ID (see :class:`ID`).  The
+    list of bugdirs allows uniqueness-checking during expansion of the
+    bugdir portion of the ID.
+
+    See Also
+    --------
+    long_to_short_user : inverse
+    short_to_long_text : conversion on a block of text
+    """
     ids = _split(id, check_length=True)
     ids[0] = _expand(ids[0], common=None,
                      other_ids=[bd.uuid for bd in bugdirs])
@@ -284,8 +454,19 @@ def short_to_long_user(bugdirs, id):
 
 
 REGEXP = '#([-a-f0-9]*)(/[-a-g0-9]*)?(/[-a-g0-9]*)?#'
+"""Regular expression for matching IDs (both short and long) in text.
+"""
 
 class IDreplacer (object):
+    """Helper class for ID replacement in text.
+
+    Reassembles the match elements from :data:`REGEXP` matching
+    into the original ID, for easier replacement.
+
+    See Also
+    --------
+    short_to_long_text, long_to_short_text
+    """
     def __init__(self, bugdirs, replace_fn, wrap=True):
         self.bugdirs = bugdirs
         self.replace_fn = replace_fn
@@ -302,13 +483,36 @@ class IDreplacer (object):
         return replacement
 
 def short_to_long_text(bugdirs, text):
+    """Convert short user IDs to long user IDs in text (see :class:`ID`).
+    The list of bugdirs allows uniqueness-checking during expansion of
+    the bugdir portion of the ID.
+
+    See Also
+    --------
+    short_to_long_user : conversion on a single ID
+    long_to_short_text : inverse
+    """
     return re.sub(REGEXP, IDreplacer(bugdirs, short_to_long_user), text)
 
 def long_to_short_text(bugdirs, text):
+    """Convert long user IDs to short user IDs in text (see :class:`ID`).
+    The list of bugdirs allows uniqueness-maintaining truncation of
+    the bugdir portion of the ID.
+
+    See Also
+    --------
+    long_to_short_user : conversion on a single ID
+    short_to_long_text : inverse
+    """
     return re.sub(REGEXP, IDreplacer(bugdirs, long_to_short_user), text)
 
 def residual(base, fragment):
-    """
+    """Split the short ID `fragment` into a portion corresponding
+    to `base`, and a portion inside `base`.
+
+    Examples
+    --------
+
     >>> residual('ABC/DEF/', '//GHI')
     ('//', 'GHI')
     >>> residual('ABC/DEF/', '/D/GHI')
@@ -326,7 +530,15 @@ def residual(base, fragment):
     return ('/'.join(root_ids), '/'.join(residual_ids))
 
 def _parse_user(id):
-    """
+    """Parse a user ID (see :class:`ID`), returning a dict of parsed
+    information.
+
+    The returned dict will contain a value for "type" (from
+    :data:`HIERARCHY`) and values for the levels that are defined.
+
+    Examples
+    --------
+
     >>> _parse_user('ABC/DEF/GHI') == \\
     ...     {'bugdir':'ABC', 'bug':'DEF', 'comment':'GHI', 'type':'comment'}
     True
@@ -361,6 +573,17 @@ def _parse_user(id):
     return ret
 
 def parse_user(bugdir, id):
+    """Parse a user ID (see :class:`ID`), returning a dict of parsed
+    information.
+
+    The returned dict will contain a value for "type" (from
+    :data:`HIERARCHY`) and values for the levels that are defined.
+
+    Notes
+    -----
+    This function tries to expand IDs before parsing, so it can handle
+    both short and long IDs successfully.
+    """
     long_id = short_to_long_user([bugdir], id)
     return _parse_user(long_id)
 
index 04ce4b340400a5a9dbd965300bbdb206046ad01b..812b0bd2a119af79fab4af579b169f0ff9ed6e50 100644 (file)
@@ -16,8 +16,7 @@
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-"""
-Define a traversable tree structure.
+"""Define :class:`Tree`, a traversable tree structure.
 """
 
 import libbe
@@ -25,12 +24,19 @@ if libbe.TESTING == True:
     import doctest
 
 class Tree(list):
-    """
-    Construct
+    """A traversable tree structure.
+
+    Examples
+    --------
+
+    Construct::
+
                +-b---d-g
              a-+   +-e
                +-c-+-f-h-i
+
     with
+
     >>> i = Tree();       i.n = "i"
     >>> h = Tree([i]);    h.n = "h"
     >>> f = Tree([h]);    f.n = "f"
@@ -43,16 +49,31 @@ class Tree(list):
     >>> a.append(c)
     >>> a.append(b)
 
+    Get the longest branch length with
+
     >>> a.branch_len()
     5
+
+    Sort the tree recursively.  Here we sort longest branch length
+    first.
+
     >>> a.sort(key=lambda node : -node.branch_len())
     >>> "".join([node.n for node in a.traverse()])
     'acfhiebdg'
+
+    And here we sort shortest branch length first.
+
     >>> a.sort(key=lambda node : node.branch_len())
     >>> "".join([node.n for node in a.traverse()])
     'abdgcefhi'
+
+    We can also do breadth-first traverses.
+
     >>> "".join([node.n for node in a.traverse(depth_first=False)])
     'abcdefghi'
+
+    Serialize the tree with depth marking branches.
+
     >>> for depth,node in a.thread():
     ...     print "%*s" % (2*depth+1, node.n)
     a
@@ -64,6 +85,10 @@ class Tree(list):
         f
           h
             i
+
+    Flattening the thread disables depth increases except at
+    branch splits.
+
     >>> for depth,node in a.thread(flatten=True):
     ...     print "%*s" % (2*depth+1, node.n)
     a
@@ -75,6 +100,9 @@ class Tree(list):
     f
     h
     i
+
+    We can also check if a node is contained in a tree.
+
     >>> a.has_descendant(g)
     True
     >>> c.has_descendant(g)
@@ -94,17 +122,22 @@ class Tree(list):
         return self.__cmp__(other) != 0
 
     def branch_len(self):
-        """
-        Exhaustive search every time == SLOW.
+        """Return the largest number of nodes from root to leaf (inclusive).
 
-        Use only on small trees, or reimplement by overriding
-        child-addition methods to allow accurate caching.
+        For the tree::
 
-        For the tree
                +-b---d-g
              a-+   +-e
                +-c-+-f-h-i
+
         this method returns 5.
+
+        Notes
+        -----
+        Exhaustive search every time == *slow*.
+
+        Use only on small trees, or reimplement by overriding
+        child-addition methods to allow accurate caching.
         """
         if len(self) == 0:
             return 1
@@ -112,18 +145,30 @@ class Tree(list):
             return 1 + max([child.branch_len() for child in self])
 
     def sort(self, *args, **kwargs):
-        """
-        This method can be slow, e.g. on a branch_len() sort, since a
-        node at depth N from the root has it's branch_len() method
-        called N times.
+        """Sort the tree recursively.
+
+        This method extends :meth:`list.sort` to Trees.
+
+        Notes
+        -----
+        This method can be slow, e.g. on a :meth:`branch_len` sort,
+        since a node at depth `N` from the root has it's
+        :meth:`branch_len` method called `N` times.
         """
         list.sort(self, *args, **kwargs)
         for child in self:
             child.sort(*args, **kwargs)
 
     def traverse(self, depth_first=True):
-        """
-        Note: you might want to sort() your tree first.
+        """Generate all the nodes in a tree, starting with the root node.
+
+        Parameters
+        ----------
+        depth_first : bool
+          Depth first by default, but you can set `depth_first` to
+          `False` for breadth first ordering.  Siblings are returned
+          in the order they are stored, so you might want to
+          :meth:`sort` your tree first.
         """
         if depth_first == True:
             yield self
@@ -139,25 +184,31 @@ class Tree(list):
                 queue.extend(node)
 
     def thread(self, flatten=False):
-        """
-        When flatten==False, the depth of any node is one greater than
-        the depth of its parent.  That way the inheritance is
-        explicit, but you can end up with highly indented threads.
-
-        When flatten==True, the depth of any node is only greater than
-        the depth of its parent when there is a branch, and the node
-        is not the last child.  This can lead to ancestry ambiguity,
-        but keeps the total indentation down.  E.g.
+        """Generate a (depth, node) tuple for every node in the tree.
+
+        When `flatten` is `False`, the depth of any node is one
+        greater than the depth of its parent.  That way the
+        inheritance is explicit, but you can end up with highly
+        indented threads.
+
+        When `flatten` is `True`, the depth of any node is only
+        greater than the depth of its parent when there is a branch,
+        and the node is not the last child.  This can lead to ancestry
+        ambiguity, but keeps the total indentation down.  For example::
+
                       +-b                  +-b-c
                     a-+-c        and     a-+
                       +-d-e-f              +-d-e-f
-        would both produce (after sorting by branch_len())
-        (0, a)
-        (1, b)
-        (1, c)
-        (0, d)
-        (0, e)
-        (0, f)
+
+        would both produce (after sorting by :meth:`branch_len`)::
+
+            (0, a)
+            (1, b)
+            (1, c)
+            (0, d)
+            (0, e)
+            (0, f)
+
         """
         stack = [] # ancestry of the current node
         if flatten == True:
@@ -182,6 +233,20 @@ class Tree(list):
             stack.append(node)
 
     def has_descendant(self, descendant, depth_first=True, match_self=False):
+        """Check if a node is contained in a tree.
+
+        Parameters
+        ----------
+        descendant : Tree
+          The potential descendant.
+        depth_first : bool
+          The search order.  Set this if you feel depth/breadth would
+          be a faster search.
+        match_self : bool
+          Set to `True` for::
+
+              x.has_descendant(x, match_self=True) -> True
+        """
         if descendant == self:
             return match_self
         for d in self.traverse(depth_first):
index d42a4f9fdde54d103b8c2fce4dd8b48073ed764a..92ca0d527b090ad4f0c27441b888099196624ba1 100644 (file)
@@ -33,11 +33,16 @@ if libbe.TESTING == True:
     import doctest
 
 class InvalidXML(ValueError):
-    """
-    Invalid XML while parsing for a *.from_xml() method.
-    type    - string identifying *, e.g. "bug", "comment", ...
-    element - ElementTree.Element instance which caused the error
-    error   - string describing the error
+    """Invalid XML while parsing for a `*.from_xml()` method.
+
+    Parameters
+    ----------
+    type : str
+        String identifying `*`, e.g. "bug", "comment", ...
+    element : :class:`ElementTree.Element`
+        ElementTree.Element instance which caused the error.
+    error : str
+        Error description.
     """
     def __init__(self, type, element, error):
         msg = 'Invalid %s xml: %s\n  %s\n' \
@@ -50,16 +55,18 @@ class InvalidXML(ValueError):
 def search_parent_directories(path, filename):
     """
     Find the file (or directory) named filename in path or in any
-    of path's parents.
-
-    e.g.
-    search_parent_directories("/a/b/c", ".be")
-    will return the path to the first existing file from
-    /a/b/c/.be
-    /a/b/.be
-    /a/.be
-    /.be
-    or None if none of those files exist.
+    of path's parents.  For example::
+
+         search_parent_directories("/a/b/c", ".be")
+
+    will return the path to the first existing file from::
+
+        /a/b/c/.be
+        /a/b/.be
+        /a/.be
+        /.be
+
+    or `None` if none of those files exist.
     """
     path = os.path.realpath(path)
     assert os.path.exists(path)
@@ -74,7 +81,11 @@ def search_parent_directories(path, filename):
         path = os.path.dirname(path)
 
 class Dir (object):
-    "A temporary directory for testing use"
+    """A temporary directory for testing use.
+
+    Make sure you run :meth:`cleanup` after you're done using the
+    directory.
+    """
     def __init__(self):
         self.path = tempfile.mkdtemp(prefix="BEtest")
         self.removed = False
@@ -86,18 +97,47 @@ class Dir (object):
         return self.path
 
 RFC_2822_TIME_FMT = "%a, %d %b %Y %H:%M:%S +0000"
+"""RFC 2822 [#]_ format string for :func:`time.strftime` and
+:func:`time.strptime`.
 
+.. [#] See `RFC 2822`_, sections 3.3 and A.1.1.
+.. _RFC 2822: http://www.faqs.org/rfcs/rfc2822.html
+"""
 
 def time_to_str(time_val):
-    """Convert a time value into an RFC 2822-formatted string.  This format
-    lacks sub-second data.
+    """Convert a time number into an RFC 2822-formatted string.
+
+    Parameters
+    ----------
+    time_val : float
+      Float seconds since the Epoc, see :func:`time.time`.
+      Note that while `time_val` may contain sub-second data,
+      the output string will not.
+
+    Examples
+    --------
+
     >>> time_to_str(0)
     'Thu, 01 Jan 1970 00:00:00 +0000'
+
+    See Also
+    --------
+    str_to_time : inverse
+    handy_time : localtime string
     """
     return time.strftime(RFC_2822_TIME_FMT, time.gmtime(time_val))
 
 def str_to_time(str_time):
     """Convert an RFC 2822-fomatted string into a time value.
+
+    Parameters
+    ----------
+    str_time : str
+      An RFC 2822-formatted string.
+
+    Examples
+    --------
+
     >>> str_to_time("Thu, 01 Jan 1970 00:00:00 +0000")
     0
     >>> q = time.time()
@@ -105,6 +145,10 @@ def str_to_time(str_time):
     True
     >>> str_to_time("Thu, 01 Jan 1970 00:00:00 -1000")
     36000
+
+    See Also
+    --------
+    time_to_str : inverse
     """
     timezone_str = str_time[-5:]
     if timezone_str != "+0000":
@@ -116,10 +160,29 @@ def str_to_time(str_time):
     return time_val + timesign*timezone
 
 def handy_time(time_val):
+    """Convert a time number into a useful localtime.
+
+    Where :func:`time_to_str` returns GMT +0000, `handy_time` returns
+    a string in local time.  This may be more accessible for the user.
+
+    Parameters
+    ----------
+    time_val : float
+      Float seconds since the Epoc, see :func:`time.time`.
+    """
     return time.strftime("%a, %d %b %Y %H:%M", time.localtime(time_val))
 
 def time_to_gmtime(str_time):
     """Convert an RFC 2822-fomatted string to a GMT string.
+
+    Parameters
+    ----------
+    str_time : str
+      An RFC 2822-formatted string.
+
+    Examples
+    --------
+
     >>> time_to_gmtime("Thu, 01 Jan 1970 00:00:00 -1000")
     'Thu, 01 Jan 1970 10:00:00 +0000'
     """
@@ -127,8 +190,23 @@ def time_to_gmtime(str_time):
     return time_to_str(time_val)
 
 def iterable_full_of_strings(value, alternative=None):
-    """
-    Require an iterable full of strings.
+    """Require an iterable full of strings.
+
+    This is useful, for example, in validating `*.extra_strings`.
+    See :attr:`libbe.bugdir.BugDir.extra_strings`
+
+    Parameters
+    ----------
+    value : list or None
+      The potential list of strings.
+    alternative
+      Allow a default (e.g. `None`), such that::
+
+        iterable_full_of_strings(value=x, alternative=x) -> True
+
+    Examples
+    --------
+
     >>> iterable_full_of_strings([])
     True
     >>> iterable_full_of_strings(["abc", "def", u"hij"])
@@ -140,21 +218,31 @@ def iterable_full_of_strings(value, alternative=None):
     """
     if value == alternative:
         return True
-    elif not hasattr(value, "__iter__"):
+    elif not hasattr(value, '__iter__'):
         return False
     for x in value:
         if type(x) not in types.StringTypes:
             return False
     return True
 
-def underlined(instring):
-    """Produces a version of a string that is underlined with '='
+def underlined(string, char='='):
+    """Produces a version of a string that is underlined.
+
+    Parameters
+    ----------
+    string : str
+      The string to underline
+    char : str
+      The character to use for the underlining.
+
+    Examples
+    --------
 
     >>> underlined("Underlined String")
     'Underlined String\\n================='
     """
-
-    return "%s\n%s" % (instring, "="*len(instring))
+    assert len(char) == 0, char
+    return '%s\n%s' % (string, char*len(string))
 
 if libbe.TESTING == True:
     suite = doctest.DocTestSuite()