aboutsummaryrefslogtreecommitdiffstats
path: root/libbe
diff options
context:
space:
mode:
Diffstat (limited to 'libbe')
-rw-r--r--libbe/__init__.py38
-rw-r--r--libbe/bugdir.py47
-rw-r--r--libbe/command/serve.py143
-rw-r--r--libbe/diff.py32
-rw-r--r--libbe/storage/__init__.py14
-rw-r--r--libbe/storage/base.py6
-rw-r--r--libbe/storage/http.py57
-rw-r--r--libbe/storage/util/config.py45
-rw-r--r--libbe/storage/util/mapfile.py28
-rw-r--r--libbe/storage/util/properties.py51
-rw-r--r--libbe/storage/util/settings_object.py95
-rw-r--r--libbe/storage/vcs/__init__.py17
-rw-r--r--libbe/storage/vcs/arch.py40
-rw-r--r--libbe/storage/vcs/base.py266
-rw-r--r--libbe/storage/vcs/bzr.py118
-rw-r--r--libbe/storage/vcs/darcs.py104
-rw-r--r--libbe/storage/vcs/git.py108
-rw-r--r--libbe/storage/vcs/hg.py86
-rw-r--r--libbe/util/id.py303
-rw-r--r--libbe/util/tree.py127
-rw-r--r--libbe/util/utility.py138
21 files changed, 1210 insertions, 653 deletions
diff --git a/libbe/__init__.py b/libbe/__init__.py
index 23acfef..d32716f 100644
--- a/libbe/__init__.py
+++ b/libbe/__init__.py
@@ -15,7 +15,39 @@
# 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
+"""
diff --git a/libbe/bugdir.py b/libbe/bugdir.py
index 9328b06..65136fe 100644
--- a/libbe/bugdir.py
+++ b/libbe/bugdir.py
@@ -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 = []
diff --git a/libbe/command/serve.py b/libbe/command/serve.py
index 7a98a47..7237343 100644
--- a/libbe/command/serve.py
+++ b/libbe/command/serve.py
@@ -14,6 +14,13 @@
# 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, \
diff --git a/libbe/diff.py b/libbe/diff.py
index 35e2151..dc13b61 100644
--- a/libbe/diff.py
+++ b/libbe/diff.py
@@ -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)
diff --git a/libbe/storage/__init__.py b/libbe/storage/__init__.py
index c3bda4b..6bceac9 100644
--- a/libbe/storage/__init__.py
+++ b/libbe/storage/__init__.py
@@ -14,6 +14,20 @@
# 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
diff --git a/libbe/storage/base.py b/libbe/storage/base.py
index ad6b291..0ae9c53 100644
--- a/libbe/storage/base.py
+++ b/libbe/storage/base.py
@@ -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 = []
diff --git a/libbe/storage/http.py b/libbe/storage/http.py
index 5606383..7ec9f54 100644
--- a/libbe/storage/http.py
+++ b/libbe/storage/http.py
@@ -21,8 +21,13 @@
# 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
diff --git a/libbe/storage/util/config.py b/libbe/storage/util/config.py
index 8526724..724d2d3 100644
--- a/libbe/storage/util/config.py
+++ b/libbe/storage/util/config.py
@@ -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")
diff --git a/libbe/storage/util/mapfile.py b/libbe/storage/util/mapfile.py
index 0b8af23..55863d7 100644
--- a/libbe/storage/util/mapfile.py
+++ b/libbe/storage/util/mapfile.py
@@ -16,10 +16,10 @@
# 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:
diff --git a/libbe/storage/util/properties.py b/libbe/storage/util/properties.py
index 55bac85..b5681b1 100644
--- a/libbe/storage/util/properties.py
+++ b/libbe/storage/util/properties.py
@@ -16,16 +16,24 @@
# 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__"):
diff --git a/libbe/storage/util/settings_object.py b/libbe/storage/util/settings_object.py
index 8434952..6e4da55 100644
--- a/libbe/storage/util/settings_object.py
+++ b/libbe/storage/util/settings_object.py
@@ -16,11 +16,12 @@
# 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 = []
diff --git a/libbe/storage/vcs/__init__.py b/libbe/storage/vcs/__init__.py
index 777c723..552d43e 100644
--- a/libbe/storage/vcs/__init__.py
+++ b/libbe/storage/vcs/__init__.py
@@ -14,6 +14,23 @@
# 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
diff --git a/libbe/storage/vcs/arch.py b/libbe/storage/vcs/arch.py
index 38b1d02..3a50414 100644
--- a/libbe/storage/vcs/arch.py
+++ b/libbe/storage/vcs/arch.py
@@ -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 = []
diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py
index 337576e..d85c94d 100644
--- a/libbe/storage/vcs/base.py
+++ b/libbe/storage/vcs/base.py
@@ -19,10 +19,9 @@
# 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')
diff --git a/libbe/storage/vcs/bzr.py b/libbe/storage/vcs/bzr.py
index 01d9948..5a62968 100644
--- a/libbe/storage/vcs/bzr.py
+++ b/libbe/storage/vcs/bzr.py
@@ -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 = []
diff --git a/libbe/storage/vcs/darcs.py b/libbe/storage/vcs/darcs.py
index fd8b7d5..4a21888 100644
--- a/libbe/storage/vcs/darcs.py
+++ b/libbe/storage/vcs/darcs.py
@@ -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 = []
diff --git a/libbe/storage/vcs/git.py b/libbe/storage/vcs/git.py
index c6638bc..4df9bc8 100644
--- a/libbe/storage/vcs/git.py
+++ b/libbe/storage/vcs/git.py
@@ -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 = []
diff --git a/libbe/storage/vcs/hg.py b/libbe/storage/vcs/hg.py
index 97fc470..9378336 100644
--- a/libbe/storage/vcs/hg.py
+++ b/libbe/storage/vcs/hg.py
@@ -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 = []
diff --git a/libbe/util/id.py b/libbe/util/id.py
index 81f5396..76079e7 100644
--- a/libbe/util/id.py
+++ b/libbe/util/id.py
@@ -15,8 +15,57 @@
# 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)
diff --git a/libbe/util/tree.py b/libbe/util/tree.py
index 04ce4b3..812b0bd 100644
--- a/libbe/util/tree.py
+++ b/libbe/util/tree.py
@@ -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):
diff --git a/libbe/util/utility.py b/libbe/util/utility.py
index d42a4f9..92ca0d5 100644
--- a/libbe/util/utility.py
+++ b/libbe/util/utility.py
@@ -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()