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