Add support for creating a PATCH body from a before and after object.
Reiewed in http://codereview.appspot.com/4532086/
diff --git a/apiclient/model.py b/apiclient/model.py
index 61a81d4..7f51d29 100644
--- a/apiclient/model.py
+++ b/apiclient/model.py
@@ -299,3 +299,47 @@
@property
def no_content_response(self):
return self._protocol_buffer()
+
+
+def makepatch(original, modified):
+ """Create a patch object.
+
+ Some methods support PATCH, an efficient way to send updates to a resource.
+ This method allows the easy construction of patch bodies by looking at the
+ differences between a resource before and after it was modified.
+
+ Args:
+ original: object, the original deserialized resource
+ modified: object, the modified deserialized resource
+ Returns:
+ An object that contains only the changes from original to modified, in a
+ form suitable to pass to a PATCH method.
+
+ Example usage:
+ item = service.activities().get(postid=postid, userid=userid).execute()
+ original = copy.deepcopy(item)
+ item['object']['content'] = 'This is updated.'
+ service.activities.patch(postid=postid, userid=userid,
+ body=makepatch(original, item)).execute()
+ """
+ patch = {}
+ for key, original_value in original.iteritems():
+ modified_value = modified.get(key, None)
+ if modified_value is None:
+ # Use None to signal that the element is deleted
+ patch[key] = None
+ elif original_value != modified_value:
+ if type(original_value) == type({}):
+ # Recursively descend objects
+ patch[key] = makepatch(original_value, modified_value)
+ else:
+ # In the case of simple types or arrays we just replace
+ patch[key] = modified_value
+ else:
+ # Don't add anything to patch if there's no change
+ pass
+ for key in modified:
+ if key not in original:
+ patch[key] = modified[key]
+
+ return patch
diff --git a/tests/test_model.py b/tests/test_model.py
new file mode 100644
index 0000000..5b9b2be
--- /dev/null
+++ b/tests/test_model.py
@@ -0,0 +1,71 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2010 Google Inc.
+#
+# 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.
+
+"""Model tests
+
+Unit tests for model utility methods.
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+import httplib2
+import unittest
+
+from apiclient.model import makepatch
+
+
+TEST_CASES = [
+ # (message, original, modified, expected)
+ ("Remove an item from an object",
+ {'a': 1, 'b': 2}, {'a': 1}, {'b': None}),
+ ("Add an item to an object",
+ {'a': 1}, {'a': 1, 'b': 2}, {'b': 2}),
+ ("No changes",
+ {'a': 1, 'b': 2}, {'a': 1, 'b': 2}, {}),
+ ("Empty objects",
+ {}, {}, {}),
+ ("Modify an item in an object",
+ {'a': 1, 'b': 2}, {'a': 1, 'b': 3}, {'b': 3}),
+ ("Change an array",
+ {'a': 1, 'b': [2, 3]}, {'a': 1, 'b': [2]}, {'b': [2]}),
+ ("Modify a nested item",
+ {'a': 1, 'b': {'foo':'bar', 'baz': 'qux'}},
+ {'a': 1, 'b': {'foo':'bar', 'baz': 'qaax'}},
+ {'b': {'baz': 'qaax'}}),
+ ("Modify a nested array",
+ {'a': 1, 'b': [{'foo':'bar', 'baz': 'qux'}]},
+ {'a': 1, 'b': [{'foo':'bar', 'baz': 'qaax'}]},
+ {'b': [{'foo':'bar', 'baz': 'qaax'}]}),
+ ("Remove item from a nested array",
+ {'a': 1, 'b': [{'foo':'bar', 'baz': 'qux'}]},
+ {'a': 1, 'b': [{'foo':'bar'}]},
+ {'b': [{'foo':'bar'}]}),
+ ("Remove a nested item",
+ {'a': 1, 'b': {'foo':'bar', 'baz': 'qux'}},
+ {'a': 1, 'b': {'foo':'bar'}},
+ {'b': {'baz': None}})
+]
+
+
+class TestPatch(unittest.TestCase):
+
+ def test_patch(self):
+ for (msg, orig, mod, expected_patch) in TEST_CASES:
+ self.assertEqual(expected_patch, makepatch(orig, mod), msg=msg)
+
+
+if __name__ == '__main__':
+ unittest.main()