Python class for accessing Rietveld reviews.
A simple python class for accessing Rietveld issues. It's using the Rietveld
interface to lazily pull down review meta data as requested by the application.
R=agable@chromium.org, iannucci@chromium.org
Review URL: https://codereview.chromium.org/20517002
git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/reviewbot@216960 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index 5425c61..28ff4db 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -39,6 +39,12 @@
finally:
sys.path = sys_path_backup
+ output.extend(input_api.canned_checks.RunUnitTestsInDirectory(
+ input_api,
+ output_api,
+ '.',
+ whitelist=[r'.*_test\.py$']))
+
return output
diff --git a/patching.py b/patching.py
new file mode 100644
index 0000000..d0cf3ed
--- /dev/null
+++ b/patching.py
@@ -0,0 +1,90 @@
+# Copyright (c) 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Copyright 2008 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utility for parsing patches, originally inspired by rietveld source."""
+
+import re
+
+
+_CHUNK_RE = re.compile(r"""
+ @@
+ \s+
+ -
+ (?: (\d+) (?: , (\d+) )?)
+ \s+
+ \+
+ (?: (\d+) (?: , (\d+) )?)
+ \s+
+ @@
+""", re.VERBOSE)
+
+
+class PatchParseError(Exception):
+ """Raised on parse errors."""
+ pass
+
+
+def ParsePatchToLines(lines):
+ """Parses a patch from an iterable type.
+
+ Args:
+ lines: The lines to parse.
+
+ Returns:
+ None on error, otherwise a list of 3-tuples:
+ (old_line_no, new_line_no, line)
+
+ A line number can be None if it doesn't exist in the old/new file.
+ """
+ # Helper function that matches a hunk header and returns line numbers.
+ def match_hunk_start(line):
+ match = _CHUNK_RE.match(line)
+ if not match:
+ raise PatchParseError(line)
+ return (int(match.groups()[0]), int(match.groups()[2]))
+
+ iterator = lines.__iter__()
+ try:
+ # Skip leading lines until after we've seen one starting with '+++'.
+ while not iterator.next().startswith('+++'):
+ pass
+
+ # Parse first hunk header.
+ old_ln, new_ln = match_hunk_start(iterator.next())
+ except StopIteration:
+ return []
+
+ # Process the actual patch lines.
+ result = []
+ for line in iterator:
+ if line[0] == '@':
+ old_ln, new_ln = match_hunk_start(line)
+ elif line[0] == '-':
+ result.append((old_ln, None, line[1:]))
+ old_ln += 1
+ elif line[0] == '+':
+ result.append((None, new_ln, line[1:]))
+ new_ln += 1
+ elif line[0] == ' ':
+ result.append((old_ln, new_ln, line[1:]))
+ old_ln += 1
+ new_ln += 1
+ else:
+ raise PatchParseError(line)
+
+ return result
diff --git a/patching_test.py b/patching_test.py
new file mode 100755
index 0000000..88823cf
--- /dev/null
+++ b/patching_test.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+# Copyright (c) 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import unittest
+
+import patching
+
+
+class TestParsePatchToLines(unittest.TestCase):
+ def test_empty(self):
+ self.assertEquals([], patching.ParsePatchToLines([]))
+
+ def test_prelude(self):
+ self.assertEquals([], patching.ParsePatchToLines("""
+Here goes some prelude.
+And it continues on for some while.
+
+--- file
++++ file
+""".splitlines()))
+
+ def test_bad_hunk_header(self):
+ with self.assertRaises(patching.PatchParseError):
+ patching.ParsePatchToLines("""
+--- file
++++ file
+@@ bad
+""".splitlines())
+
+ def test_multiple_files(self):
+ with self.assertRaises(patching.PatchParseError):
+ patching.ParsePatchToLines("""
+diff -urN test1/bar test2/bar
+--- test1/bar 2013-08-08 14:15:46.604119530 +0200
++++ test2/bar 2013-08-08 14:15:49.814145535 +0200
+@@ -1 +1 @@
+-foo
++bar
+diff -urN test1/foo test2/foo
+--- test1/foo 2013-08-08 14:15:36.044033982 +0200
++++ test2/foo 2013-08-08 14:15:42.204083886 +0200
+@@ -1 +1 @@
+-foo
++bar
+""".splitlines())
+
+ def test_simple(self):
+ self.assertEquals([
+ (1, 1, 'common line'),
+ (2, None, 'old line'),
+ (None, 2, 'new line'),
+ (3, 3, 'common line'),
+ ], patching.ParsePatchToLines("""
+--- old 2013-08-08 14:05:18.539090366 +0200
++++ new 2013-08-08 14:05:18.539090366 +0200
+@@ -1,3 +1,3 @@
+ common line
+-old line
++new line
+ common line
+""".splitlines()))
+
+ def test_multiple_hunks(self):
+ self.assertEquals([
+ (None, 1, 'prepended line'),
+ (1, 2, ''),
+ (2, 3, ''),
+ (3, 4, ''),
+ (8, 9, ''),
+ (9, 10, ''),
+ (10, 11, ''),
+ (None, 12, 'appended line'),
+ ], patching.ParsePatchToLines("""
+--- old 2013-08-08 14:10:10.391408985 +0200
++++ new 2013-08-08 14:10:15.511449623 +0200
+@@ -1,3 +1,4 @@
++prepended line
+
+
+
+@@ -8,3 +9,4 @@
+
+
+
++appended line
+""".splitlines()))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/review.py b/review.py
new file mode 100644
index 0000000..9ad3ea7
--- /dev/null
+++ b/review.py
@@ -0,0 +1,72 @@
+# Copyright (c) 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import json
+
+import patching
+import util
+
+
+class Patch(object):
+ """Helper class for lazily loading and parsing patch data."""
+
+ def __init__(self, rietveld, issue_id, patchset_id, patch_id):
+ self.rietveld = rietveld
+ self.issue_id = issue_id
+ self.patchset_id = patchset_id
+ self.patch_id = patch_id
+
+ @util.lazy_property
+ def raw(self):
+ return self.rietveld.post_data(
+ 'download/issue%s_%s_%s.diff' %
+ (self.issue_id, self.patchset_id, self.patch_id))
+
+ @util.lazy_property
+ def lines(self):
+ return patching.ParsePatchToLines(self.raw.splitlines())
+
+
+class Review(object):
+ """Represents a code review.
+
+ Information from rietveld can be obtained via the following properties:
+ - |issue_id| is the issue identifier.
+ - |issue_data| contains issue meta data as retrieved from rietveld. The data
+ is pulled lazily from the rietveld API on first access.
+ - |patchsets| has lazily-pulled patchset meta data, indexed by patchset IDa.
+
+ The subclass may then do its processing and trigger any actions. In
+ particular, the |rietveld| object may be used to update rietveld issue state.
+ """
+ def __init__(self, rietveld, issue_id):
+ self.rietveld = rietveld
+ self.issue_id = issue_id
+
+ @util.lazy_property
+ def issue_data(self):
+ json_data = self.rietveld.post_data('api/%s?messages=true' % self.issue_id)
+ data = json.loads(json_data)
+ data['messages'] = [util.ObjectDict(msg) for msg in data['messages']]
+ return util.ObjectDict(data)
+
+ @util.lazy_property
+ def patchsets(self):
+ def retrieve_patchset(ps):
+ json_patchset_data = self.rietveld.post_data('api/%s/%s' %
+ (self.issue_id, ps))
+ patchset_data = json.loads(json_patchset_data)
+
+ # Amend the files property so it can lazily load and return patch data.
+ for file_data in patchset_data.get('files', {}).values():
+ file_data['patch'] = Patch(self.rietveld, self.issue_id, ps,
+ file_data['id'])
+
+ return util.ObjectDict(patchset_data)
+
+ return util.LazyDict(retrieve_patchset)
+
+ @util.lazy_property
+ def latest_patchset(self):
+ return self.patchsets[self.issue_data.patchsets[-1]]