aboutsummaryrefslogtreecommitdiffstats
path: root/libbe
diff options
context:
space:
mode:
Diffstat (limited to 'libbe')
-rw-r--r--libbe/__init__.py38
-rw-r--r--libbe/bug.py25
-rw-r--r--libbe/bugdir.py57
-rw-r--r--libbe/command/assign.py2
-rw-r--r--libbe/command/serve.py146
-rw-r--r--libbe/comment.py37
-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/ui/util/user.py89
-rw-r--r--libbe/util/id.py303
-rw-r--r--libbe/util/tree.py127
-rw-r--r--libbe/util/utility.py138
25 files changed, 1315 insertions, 714 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/bug.py b/libbe/bug.py
index 0b40921..8bf32dd 100644
--- a/libbe/bug.py
+++ b/libbe/bug.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 the Bug class for representing bugs.
+"""Define the :class:`Bug` class for representing bugs.
"""
import copy
@@ -122,7 +121,11 @@ load_status(active_status_def, inactive_status_def)
class Bug (settings_object.SavedSettingsObject):
- """
+ """A bug (or issue) is a place to store attributes and attach
+ :class:`~libbe.comment.Comment`\s. In mailing-list terms, a bug is
+ analogous to a thread. Bugs are normally stored in
+ :class:`~libbe.bugdir.BugDir`\s.
+
>>> b = Bug()
>>> print b.status
open
@@ -132,6 +135,7 @@ class Bug (settings_object.SavedSettingsObject):
There are two formats for time, int and string. Setting either
one will adjust the other appropriately. The string form is the
one stored in the bug's settings file on disk.
+
>>> print type(b.time)
<type 'int'>
>>> print type(b.time_string)
@@ -333,7 +337,7 @@ class Bug (settings_object.SavedSettingsObject):
return istring + sep.join(lines).rstrip('\n')
def from_xml(self, xml_string, verbose=True):
- """
+ u"""
Note: If a bug uuid is given, set .alt_id to it's value.
>>> bugA = Bug(uuid="0123", summary="Need to test Bug.from_xml()")
>>> bugA.date = "Thu, 01 Jan 1970 00:00:00 +0000"
@@ -410,6 +414,7 @@ class Bug (settings_object.SavedSettingsObject):
Add a comment too the current bug, under the parent specified
by comment.in_reply_to.
Note: If a bug uuid is given, set .alt_id to it's value.
+
>>> bugA = Bug(uuid='0123', summary='Need to test Bug.add_comment()')
>>> bugA.creator = 'Jack'
>>> commA = bugA.comment_root.new_reply(body='comment A')
@@ -506,6 +511,7 @@ class Bug (settings_object.SavedSettingsObject):
"""
Merge info from other into this bug. Overrides any attributes
in self that are listed in other.explicit_attrs.
+
>>> bugA = Bug(uuid='0123', summary='Need to test Bug.merge()')
>>> bugA.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
>>> bugA.creator = 'Frank'
@@ -712,6 +718,7 @@ def cmp_severity(bug_1, bug_2):
"""
Compare the severity levels of two bugs, with more severe bugs
comparing as less.
+
>>> bugA = Bug()
>>> bugB = Bug()
>>> bugA.severity = bugB.severity = "wishlist"
@@ -730,8 +737,9 @@ def cmp_severity(bug_1, bug_2):
def cmp_status(bug_1, bug_2):
"""
- Compare the status levels of two bugs, with more 'open' bugs
+ Compare the status levels of two bugs, with more "open" bugs
comparing as less.
+
>>> bugA = Bug()
>>> bugB = Bug()
>>> bugA.status = bugB.status = "open"
@@ -751,9 +759,10 @@ def cmp_status(bug_1, bug_2):
def cmp_attr(bug_1, bug_2, attr, invert=False):
"""
- Compare a general attribute between two bugs using the conventional
- comparison rule for that attribute type. If invert == True, sort
- *against* that convention.
+ Compare a general attribute between two bugs using the
+ conventional comparison rule for that attribute type. If
+ ``invert==True``, sort *against* that convention.
+
>>> attr="severity"
>>> bugA = Bug()
>>> bugB = Bug()
diff --git a/libbe/bugdir.py b/libbe/bugdir.py
index 3c3afa0..65136fe 100644
--- a/libbe/bugdir.py
+++ b/libbe/bugdir.py
@@ -19,8 +19,7 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-"""
-Define the BugDir class for representing bug comments.
+"""Define the :class:`BugDir` class for storing a collection of bugs.
"""
import copy
@@ -49,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)
@@ -82,15 +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):
- """
- TODO: simple bugdir manipulation examples...
+ """A BugDir is a container for :class:`~libbe.bug.Bug`\s, with some
+ additional attributes.
+
+ 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 = []
@@ -344,7 +330,8 @@ class RevisionedBugDir (BugDir):
if libbe.TESTING == True:
class SimpleBugDir (BugDir):
"""
- For testing. Set memory=True for a memory-only bugdir.
+ For testing. Set ``memory=True`` for a memory-only bugdir.
+
>>> bugdir = SimpleBugDir()
>>> uuids = list(bugdir.uuids())
>>> uuids.sort()
diff --git a/libbe/command/assign.py b/libbe/command/assign.py
index 2430d23..6abf05e 100644
--- a/libbe/command/assign.py
+++ b/libbe/command/assign.py
@@ -24,7 +24,7 @@ import libbe.command.util
class Assign (libbe.command.Command):
- """Assign an individual or group to fix a bug
+ u"""Assign an individual or group to fix a bug
>>> import sys
>>> import libbe.bugdir
diff --git a/libbe/command/serve.py b/libbe/command/serve.py
index 43e07cc..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,10 +302,7 @@ class AuthenticationApp (WSGI_Object):
e.code, e.msg, e.headers)
def authenticate(self, environ):
- """Handle user-authentication sent in the 'Authorization' header.
-
- Basic HTTP/1.0 Authentication
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ """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
@@ -302,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)
@@ -309,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:
@@ -342,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):
@@ -472,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/
@@ -483,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.
"""
@@ -508,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
@@ -681,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'
@@ -1044,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()
@@ -1083,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)
@@ -1108,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/comment.py b/libbe/comment.py
index dd245de..d8632a4 100644
--- a/libbe/comment.py
+++ b/libbe/comment.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 the Comment class for representing bug comments.
+"""Define the :class:`Comment` class for representing bug comments.
"""
import base64
@@ -101,7 +100,10 @@ def save_comments(bug):
class Comment (Tree, settings_object.SavedSettingsObject):
- """
+ """Comments are a notes that attach to :class:`~libbe.bug.Bug`\s in
+ threaded trees. In mailing-list terms, a comment is analogous to
+ a single part of an email.
+
>>> c = Comment()
>>> c.uuid != None
True
@@ -194,19 +196,19 @@ class Comment (Tree, settings_object.SavedSettingsObject):
def __init__(self, bug=None, uuid=None, from_storage=False,
in_reply_to=None, body=None, content_type=None):
"""
- Set from_storage=True to load an old comment.
- Set from_storage=False to create a new comment.
+ Set ``from_storage=True`` to load an old comment.
+ Set ``from_storage=False`` to create a new comment.
- The uuid option is required when from_storage==True.
+ The ``uuid`` option is required when ``from_storage==True``.
The in_reply_to, body, and content_type options are only used
- if from_storage==False (the default). When
- from_storage==True, they are loaded from the bug database.
- content_type decides if the body should be run through
- libbe.util.id.short_to_long_text() before saving. See
- ._set_comment_body() for details.
+ if ``from_storage==False`` (the default). When
+ ``from_storage==True``, they are loaded from the bug database.
+ ``content_type`` decides if the body should be run through
+ :func:`util.id.short_to_long_text` before saving. See
+ :meth:`_set_comment_body` for details.
- in_reply_to should be the uuid string of the parent comment.
+ ``in_reply_to`` should be the uuid string of the parent comment.
"""
Tree.__init__(self)
settings_object.SavedSettingsObject.__init__(self)
@@ -267,6 +269,7 @@ class Comment (Tree, settings_object.SavedSettingsObject):
def safe_in_reply_to(self):
"""
Return self.in_reply_to, except...
+
* if no comment matches that id, in which case return None.
* if that id matches another comments .alt_id, in which case
return the matching comments .uuid.
@@ -337,7 +340,7 @@ class Comment (Tree, settings_object.SavedSettingsObject):
return istring + sep.join(lines).rstrip('\n')
def from_xml(self, xml_string, verbose=True):
- """
+ u"""
Note: If alt-id is not given, translates any <uuid> fields to
<alt-id> fields.
>>> commA = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n")
@@ -412,6 +415,7 @@ class Comment (Tree, settings_object.SavedSettingsObject):
"""
Merge info from other into this comment. Overrides any
attributes in self that are listed in other.explicit_attrs.
+
>>> commA = Comment(bug=None, body='Some insightful remarks')
>>> commA.uuid = '0123'
>>> commA.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
@@ -621,7 +625,7 @@ class Comment (Tree, settings_object.SavedSettingsObject):
"""
Save any loaded contents to storage.
- However, if self.storage.is_writeable() == True, then any
+ However, if ``self.storage.is_writeable() == True``, then any
changes are automatically written to storage as soon as they
happen, so calling this method will just waste time (unless
something else has been messing with your stored files).
@@ -666,8 +670,8 @@ class Comment (Tree, settings_object.SavedSettingsObject):
return reply
def comment_from_uuid(self, uuid, match_alt_id=True):
- """
- Use a uuid to look up a comment.
+ """Use a uuid to look up a comment.
+
>>> a = Comment(bug=None, uuid="a")
>>> b = a.new_reply()
>>> b.uuid = "b"
@@ -708,6 +712,7 @@ def cmp_attr(comment_1, comment_2, attr, invert=False):
Compare a general attribute between two comments using the conventional
comparison rule for that attribute type. If invert == True, sort
*against* that convention.
+
>>> attr="author"
>>> commentA = Comment()
>>> commentB = Comment()
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/ui/util/user.py b/libbe/ui/util/user.py
index 98f16a6..460a1dd 100644
--- a/libbe/ui/util/user.py
+++ b/libbe/ui/util/user.py
@@ -14,13 +14,21 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-"""
-Tools for getting, setting, creating, and parsing the user's id. For
-example,
- 'John Doe <jdoe@example.com>'
-Note that the Arch VCS backend *enforces* ids with this format.
+"""Tools for getting, setting, creating, and parsing the user's ID.
+
+IDs will look like 'John Doe <jdoe@example.com>'. Note that the
+:mod:`libbe.storage.vcs.arch <Arch VCS backend>` *enforces* IDs with
+this format.
+
+Do not confuse the user IDs discussed in this module, which refer to
+humans, with the "user IDs" discussed in :mod:`libbe.util.id`, which
+are human-readable tags refering to objects.
"""
+try:
+ from email.utils import formataddr, parseaddr
+except ImportErrror: # adjust to old python < 2.5
+ from email.Utils import formataddr, parseaddr
import os
import re
from socket import gethostname
@@ -29,6 +37,8 @@ import libbe
import libbe.storage.util.config
def get_fallback_username():
+ """Return a username extracted from environmental variables.
+ """
name = None
for env in ["LOGNAME", "USERNAME"]:
if os.environ.has_key(env):
@@ -38,52 +48,69 @@ def get_fallback_username():
return name
def get_fallback_email():
+ """Return an email address extracted from environmental variables.
+ """
hostname = gethostname()
name = get_fallback_username()
return "%s@%s" % (name, hostname)
def create_user_id(name, email=None):
- """
+ """Create a user ID string from given `name` and `email` strings.
+
+ Examples
+ --------
+
>>> create_user_id("John Doe", "jdoe@example.com")
'John Doe <jdoe@example.com>'
>>> create_user_id("John Doe")
'John Doe'
+
+ See Also
+ --------
+ parse_user_id : inverse
"""
assert len(name) > 0
if email == None or len(email) == 0:
return name
else:
- return "%s <%s>" % (name, email)
+ return formataddr((name, email))
def parse_user_id(value):
- """
+ """Parse a user ID string into `name` and `email` strings.
+
+ Examples
+ --------
+
>>> parse_user_id("John Doe <jdoe@example.com>")
('John Doe', 'jdoe@example.com')
>>> parse_user_id("John Doe")
('John Doe', None)
- >>> try:
- ... parse_user_id("John Doe <jdoe@example.com><what?>")
- ... except AssertionError:
- ... print "Invalid match"
- Invalid match
+ >>> parse_user_id("John Doe <jdoe@example.com><what?>")
+ ('John Doe', 'jdoe@example.com')
+
+ See Also
+ --------
+ create_user_id : inverse
"""
- emailexp = re.compile("(.*) <([^>]*)>(.*)")
- match = emailexp.search(value)
- if match == None:
- email = None
- name = value
- else:
- assert len(match.groups()) == 3
- assert match.groups()[2] == "", match.groups()
- email = match.groups()[1]
- name = match.groups()[0]
- assert name != None
- assert len(name) > 0
- return (name, email)
+ if '<' not in value:
+ return (value, None)
+ return parseaddr(value)
def get_user_id(storage=None):
- """
- Sometimes the storage will also keep track of the user id (e.g. most VCSs).
+ """Return a user ID, checking a list of possible sources.
+
+ The source order is:
+
+ 1. Global BE configuration.
+ 2. `storage.get_user_id`, if that function is defined.
+ 3. :func:`get_fallback_username` and :func:`get_fallback_email`.
+
+ Notes
+ -----
+ Sometimes the storage will keep track of the user ID (e.g. most
+ VCSs, see :meth:`libbe.storage.vcs.base.VCS.get_user_id`). If so,
+ we prefer that ID to the fallback, since the user has likely
+ configured it directly.
"""
user = libbe.storage.util.config.get_val('user')
if user != None:
@@ -98,6 +125,10 @@ def get_user_id(storage=None):
return user
def set_user_id(user_id):
- """
+ """Set the user ID in a user's BE configuration.
+
+ See Also
+ --------
+ libbe.storage.util.config.set_val
"""
user = libbe.storage.util.config.set_val('user', user_id)
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()