# HG changeset patch # User Steve Losh # Date 1254973110 14400 # Node ID 789e5765c9ff940d20949e7d5889fb216a7a9a8f # Parent 08528309f7cd38cba0fa25d1e2a260f262e106cf 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. diff -r 08528309f7cd -r 789e5765c9ff review/api.py --- 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,