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 @@ class ProtocolBufferModel(BaseModel): @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()