789e5765c9ff

Start adding some documentation to the internals of the API.

I also managed to sneak a doctest in there which found a bug.
The bug is now fixed.
Awesome.
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Wed, 07 Oct 2009 23:38:30 -0400
parents 08528309f7cd
children 62c8ba974c19
branches/tags (none)
files review/api.py

Changes

--- a/review/api.py	Wed Oct 07 19:08:50 2009 -0400
+++ b/review/api.py	Wed Oct 07 23:38:30 2009 -0400
@@ -1,7 +1,6 @@
 from __future__ import with_statement
 
-'''The review data structures.
-'''
+"""The data structures used by hg-review."""
 
 import os
 import messages, templates
@@ -19,15 +18,15 @@
     
 
 class SignoffExists(Exception):
-    '''Raised when trying to signoff twice without forcing.'''
+    """Raised when trying to signoff twice without forcing."""
     pass
 
 class CannotDeleteObject(Exception):
-    '''Raised when trying to delete an object that does not support deletion.'''
+    """Raised when trying to delete an object that does not support deletion."""
     pass
 
 class FileNotInChangeset(Exception):
-    '''Raised when trying to add a comment on a file not in the changeset.'''
+    """Raised when trying to add a comment on a file not in the changeset."""
     def __init__(self, filename):
         super(FileNotInChangeset, self).__init__()
         self.filename = filename
@@ -35,17 +34,38 @@
 
 
 def _split_path_dammit(p):
+    """Take a file path (from the current platform) and split it.  Really.
+    
+    os.path doesn't seem to have an easy way to say "Split this path into a
+    list of pieces."
+    
+    >>> _split_path_dammit('')
+    []
+    >>> _split_path_dammit('one')
+    ['one']
+    >>> _split_path_dammit('one/two/three')
+    ['one', 'two', 'three']
+    >>> _split_path_dammit('one/two/three/')
+    ['one', 'two', 'three']
+    >>> _split_path_dammit('one/two/three.py')
+    ['one', 'two', 'three.py']
+    
+    """
     def _spd(p):
         p, i = os.path.split(p)
-        while i:
+        while i or p:
             yield i
             p, i = os.path.split(p)
     
-    return list(_spd(p))[::-1]
+    return filter(None, list(_spd(p)))[::-1]
 
 def _parse_hgrf(repo):
-    """Parse the .hgreview file and return the data inside."""
+    """Parse the .hgreview file and return the data inside.
     
+    The .hgreview file will be pulled from the tip revision of the given
+    repository.  If it is not committed it will not be found!
+    
+    """
     data = {}
     hgrd = repo['tip']['.hgreview'].data().split('\n')
     lines = [line for line in hgrd if line.strip()]
@@ -59,12 +79,16 @@
     return data
 
 def _commitfunc(ui, repo, message, match, opts):
+    """A function used by the guts of Mercurial.
+    
+    Mercurial needs a "commit function" parameter when using cmdutil.commit.
+    This is a simple function for *only* that purpose.
+    
+    """
     return repo.commit(message, opts.get('user'), opts.get('date'), match)
 
-def _match(start):
-    return lambda fn: fn.startswith(start)
-
 def _parse_data(data):
+    """Parse the data (string) of a stored _ReviewObject and return a dict."""
     meta, _, message = data.partition('\n\n')
     
     data = {}
@@ -76,14 +100,26 @@
 
 
 def sanitize_path(p, repo=None):
-    '''Take a path specific to the current platform and convert it.'''
+    """Sanitize a (platform-specific) path.
+    
+    If no repository is given, the path's separators will be replaced with
+    forward slashes (the form Mercurial uses internally).
+    
+    If a repository is given, the result will be relative to the root of the
+    repository.  This is useful for turning relative paths into normalized
+    paths that can be used to look up files from a changectx.
+    
+    This function is idempotent.  If you sanitize a path multiple times
+    against the same repository the result will not change.
+    
+    """
     if repo:
         p = os.path.relpath(os.path.realpath(p), start=repo.root)
     return '/'.join(_split_path_dammit(p))
 
 
 class ReviewDatastore(object):
-    '''The data store for all the reviews so far.'''
+    """The data store for all the reviews so far."""
     def __init__(self, ui, repo, lpath=None, rpath=None, create=False):
         self.ui = ui
         
@@ -111,13 +147,13 @@
             repo.add(['.hgreview'])
     
     def __getitem__(self, rev):
-        '''Return a ReviewChangeset for the given revision.'''
+        """Return a ReviewChangeset for the given revision."""
         node = hex(self.target[rev].node())
         return ReviewChangeset(self.ui, self.repo, self.target, node)
     
 
 class ReviewChangeset(object):
-    '''The review data about one changeset in the target repository.'''
+    """The review data about one changeset in the target repository."""
     def __init__(self, ui, repo, target, node):
         self.repo = repo
         self.target = target
@@ -125,6 +161,8 @@
         self.node = node
         
         if '%s/.exists' % self.node in self.repo['tip']:
+            _match = lambda p: lambda fn: fn.startswith(p)
+            
             relevant = filter(_match(node), self.repo['tip'])
             commentfns = filter(_match('%s/comments' % node), relevant)
             signofffns = filter(_match('%s/signoffs' % node), relevant)
@@ -155,7 +193,7 @@
                   'addremove': True, })
     
     def add_signoff(self, message, opinion='', force=False):
-        '''Add (and commit) a signoff for the given revision.'''
+        """Add (and commit) a signoff for the given revision."""
         existing = filter(lambda s: s.author == self.ui.username(), self.signoffs)
         
         if existing:
@@ -168,7 +206,7 @@
         signoff.commit(self.ui, self.repo)
     
     def add_comment(self, message, filename='', lines=[]):
-        '''Add (and commit) a comment for the given file and lines.'''
+        """Add (and commit) a comment for the given file and lines."""
         if filename and filename not in self.target[self.node].files():
             raise FileNotInChangeset(filename)
         
@@ -177,7 +215,7 @@
         comment.commit(self.ui, self.repo)
     
     def full_diffs(self, filenames=None, opts={}):
-        '''Return diffs of the given files.'''
+        """Return diffs of the given files."""
         
         target_files = self.target[self.node].files()
         if not filenames:
@@ -206,14 +244,14 @@
     
 
 class _ReviewObject(object):
-    '''Some kind of object.'''
+    """Some kind of object."""
     def __init__(self, container, commit_message, delete_message=None):
         self.container = container
         self.commit_message = commit_message
         self.delete_message = delete_message
     
     def commit(self, ui, repo):
-        '''Write and commit this object to the given repo.'''
+        """Write and commit this object to the given repo."""
         
         path = os.path.join(repo.root, self.node, self.container)
         if not os.path.exists(path):
@@ -230,7 +268,7 @@
             { 'message': self.commit_message % self.node, 'addremove': True, })
     
     def delete(self, ui, repo):
-        '''Delete and commit this object in the given repo.'''
+        """Delete and commit this object in the given repo."""
         
         if not self.delete_message:
             raise CannotDeleteObject
@@ -246,7 +284,7 @@
     
 
 class ReviewComment(_ReviewObject):
-    '''A single review comment.'''
+    """A single review comment."""
     def __init__(self, author, datetime, node, filename, lines, message, **extra):
         super(ReviewComment, self).__init__(
             container='comments', commit_message=messages.COMMIT_COMMENT,
@@ -277,7 +315,7 @@
     
 
 class ReviewSignoff(_ReviewObject):
-    '''A single review signoff.'''
+    """A single review signoff."""
     def __init__(self, author, datetime, node, opinion, message, **extra):
         super(ReviewSignoff, self).__init__(
             container='signoffs', commit_message=messages.COMMIT_SIGNOFF,