From 7e889ee3b0e30549d473fc39c25cfb84a285e34c Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Thu, 19 Dec 2013 18:49:25 +0000 Subject: [PATCH 01/10] Ensure uploaded filename is correct --- tests/unit/test_photos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index 9b10a77..d11f15d 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -419,7 +419,7 @@ def test_photo_upload(self, mock_post): files = mock_post.call_args[1]["files"] self.assertEqual(endpoint, ("/photo/upload.json",)) self.assertEqual(title, "Test") - self.assertIn("photo", files) + self.assertEqual(files["photo"].name, self.test_file) self.assertEqual(result.get_fields(), self.test_photos_dict[0]) class TestPhotoUploadEncoded(TestPhotos): From d6d84f4e0b8128d7947c93e55004cbd781dcdd63 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Thu, 19 Dec 2013 18:50:12 +0000 Subject: [PATCH 02/10] Add tests for unicode filename uploads --- "tests/data/test_\303\274nicode_photo.jpg" | Bin 0 -> 1657 bytes tests/functional/test_photos.py | 21 +++++++++ .../unit/data/\303\274nicode_test_file.txt" | 1 + tests/unit/test_cli.py | 41 ++++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 "tests/data/test_\303\274nicode_photo.jpg" create mode 100644 "tests/unit/data/\303\274nicode_test_file.txt" diff --git "a/tests/data/test_\303\274nicode_photo.jpg" "b/tests/data/test_\303\274nicode_photo.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..bc469e3216330372c736425b3235087452058d9c GIT binary patch literal 1657 zcmb7Ec~FyA5dXd}2_%GsFOU$Fa6~~1f)b7tuz=hI)Dbz#R1vwXfDs}B1%ZKx2L*(1 zIovl5fsBG&(tsR-H3B1uEtf)yC=8&YDC&sm2P18#)3MWi@1M7C-`m~a?*62s(rEy8 zc3?RGAP53Z$O5DjfGvQ6p!JOi8rc{u27^XpQPZj%3gIeKG54Aj!N_jsB7STO=a{GF}|4%V!M?i4vmI6$Czv5BS#tLF)532UuS zgF}{bRX(;H7YuvF=HDg7c#Zni`&M{rTbenD6;rR&H0%#4a!1&=c`$E*#UB>FxNaF%h@gW6kmt|vk!{S&-H8_^@FS%Lmv zFn~4S9vebB|*XGN$_uyt%_f>(K;(tL-!IYn8o zRN0XsrF>L2@ksxff`Mo^w+@?+X2&OIEE)fz%B6L1yEJOo#PPJMB$2zi4eM=-*F>v( zujzI5=q6*axqG?(`@r+zsqgv@J!6_+Q|NKY9xbbhQyH40S9k{uM~_~bG>40|9`u{B z*?MK33zQ98m!J{tzFgxPIV$?_WOwx+md~(YIox;;Q zBjk8Hmpm#?{a*N>UJ@SPmTcgc5_Tt=I6t~v-`+Zllv5i<`VvCRAk(o~EfPS7HLYD4 zzMRCem-SuCyv_EtqVr92_Ouj>Tk%5USl6N-2`9~I^J!mg*Y`8rooTn$Xfx&c8W&KA z8*$HzsQ|Ev;}^Qbny37^M@f-B9`D5Gw*3%gwd+v7(~}IAPip0?F1YeA@`Ex!qeXsA{JS$=q1c7~FEs*nzAjs*4&n zqnvNJxUMo?oaxl5<}KWrv`%#YLV5ba?dNl|`8aADE z;mAE?-6!1))s+Hmr{Ai*o36GJT{gaQzm|YzMqCPK+;G7gwBX{VzOC@rPfwSQ{Q=4V BUjG09 literal 0 HcmV?d00001 diff --git a/tests/functional/test_photos.py b/tests/functional/test_photos.py index 1fe409b..0f5c867 100644 --- a/tests/functional/test_photos.py +++ b/tests/functional/test_photos.py @@ -121,6 +121,27 @@ def test_upload_from_url(self): photos[1].delete() self.photos[0].update(permission=False) + # Unicode filename upload not working due to frontend bug 1433 + @unittest.expectedFailure + def test_upload_unicode_filename(self): + """Test that a photo with a unicode filename can be uploaded""" + ret_val = self.client.photo.upload(u"tests/data/test_\xfcnicode_photo.jpg", + title=self.TEST_TITLE) + # Check that there are now four photos + self.photos = self.client.photos.list() + self.assertEqual(len(self.photos), 4) + + # Check that the upload return value was correct + pathOriginals = [photo.pathOriginal for photo in self.photos] + self.assertIn(ret_val.pathOriginal, pathOriginals) + + # Delete the photo + ret_val.delete() + + # Check that it's gone + self.photos = self.client.photos.list() + self.assertEqual(len(self.photos), 3) + def test_update(self): """ Update a photo by editing the title """ title = "\xfcmlaut" # umlauted umlaut diff --git "a/tests/unit/data/\303\274nicode_test_file.txt" "b/tests/unit/data/\303\274nicode_test_file.txt" new file mode 100644 index 0000000..4fff881 --- /dev/null +++ "b/tests/unit/data/\303\274nicode_test_file.txt" @@ -0,0 +1 @@ +Test File diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 22daa00..08f81aa 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -23,6 +23,8 @@ def raise_exception(_): class TestCli(unittest.TestCase): test_file = os.path.join("tests", "unit", "data", "test_file.txt") + test_unicode_file = os.path.join("tests", "unit", "data", + "\xfcnicode_test_file.txt") @mock.patch.object(trovebox.main.trovebox, "Trovebox") @mock.patch('sys.stdout', new_callable=io.StringIO) @@ -107,6 +109,45 @@ def test_post_missing_files(self, _, mock_trovebox): with self.assertRaises(IOError): main(["-X", "POST", "-F", "photo=@%s.missing" % self.test_file]) + @mock.patch.object(trovebox.main.trovebox, "Trovebox") + @mock.patch('sys.stdout', new_callable=io.StringIO) + def test_post_unicode_files(self, _, mock_trovebox): + """Check that unicode filenames are posted correctly""" + post = mock_trovebox.return_value.post + + # Python 2.x provides encoded commandline arguments + file_param = "photo=@%s" % self.test_unicode_file + if sys.version < '3': + file_param = file_param.encode(sys.getfilesystemencoding()) + + main(["-X", "POST", "-F", "photo=@%s" % self.test_unicode_file]) + # It's not possible to directly compare the file object, + # so check it manually + files = post.call_args[1]["files"] + self.assertEqual(list(files.keys()), ["photo"]) + self.assertEqual(files["photo"].name, self.test_unicode_file) + + @unittest.skipIf(sys.version >= '3', + "Python3 only uses unicode commandline arguments") + @mock.patch('trovebox.main.sys.getfilesystemencoding') + @mock.patch.object(trovebox.main.trovebox, "Trovebox") + @mock.patch('sys.stdout', new_callable=io.StringIO) + def test_post_utf8_files(self, _, mock_trovebox, mock_getfilesystemencoding): + """Check that utf-8 encoded filenames are posted correctly""" + post = mock_trovebox.return_value.post + # Make the system think its filesystemencoding is utf-8 + mock_getfilesystemencoding.return_value = "utf-8" + + file_param = "photo=@%s" % self.test_unicode_file + file_param = file_param.encode("utf-8") + + main(["-X", "POST", "-F", file_param]) + # It's not possible to directly compare the file object, + # so check it manually + files = post.call_args[1]["files"] + self.assertEqual(list(files.keys()), ["photo"]) + self.assertEqual(files["photo"].name, self.test_unicode_file) + @mock.patch.object(sys, "exit", raise_exception) @mock.patch('sys.stderr', new_callable=io.StringIO) def test_unknown_arg(self, mock_stderr): From 76411cb1660405c72baa6378b703ee8da27a706b Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Thu, 19 Dec 2013 18:52:01 +0000 Subject: [PATCH 03/10] Support unicode filenames Python2 encodes commandline parameters based on the default system encoding. We must explicitly decode this to unicode. Python3 uses unicode filenames by default, so no action needed. --- trovebox/main.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/trovebox/main.py b/trovebox/main.py index 1d8bb54..b500c2e 100644 --- a/trovebox/main.py +++ b/trovebox/main.py @@ -123,7 +123,14 @@ def extract_files(params): updated_params = {} for name in params: if name == "photo" and params[name].startswith("@"): - files[name] = open(os.path.expanduser(params[name][1:]), 'rb') + filename = params[name][1:] + + # Python2 uses encoded commandline parameters. + # Decode to Unicode if necessary. + if isinstance(filename, bytes): + filename = filename.decode(sys.getfilesystemencoding()) + + files[name] = open(os.path.expanduser(filename), 'rb') else: updated_params[name] = params[name] From 43f533a419b62f612a91c5ca1fb4be0727f22c53 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Fri, 31 Jan 2014 18:57:33 +0000 Subject: [PATCH 04/10] Ensure lists inside parameters are UTF-8 encoded Recurse properly into embedded lists Ensure all parameter contents are UTF-8 encoded --- tests/unit/test_http.py | 6 +++-- trovebox/http.py | 58 +++++++++++++++++++++++------------------ 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py index cfa3cb4..e625b9a 100644 --- a/tests/unit/test_http.py +++ b/tests/unit/test_http.py @@ -193,7 +193,8 @@ def test_get_parameter_processing(self): self.client.get(self.test_endpoint, photo=photo, album=album, tag=tag, list_=[photo, album, tag], - list2=["1", "2", "3"], + list2=["1", False, 3], + unicode_list=["1", "2", "\xfcmlaut"], boolean=True, unicode_="\xfcmlaut") params = self._last_request().querystring @@ -201,7 +202,8 @@ def test_get_parameter_processing(self): self.assertEqual(params["album"], ["album_id"]) self.assertEqual(params["tag"], ["tag_id"]) self.assertEqual(params["list_"], ["photo_id,album_id,tag_id"]) - self.assertEqual(params["list2"], ["1,2,3"]) + self.assertEqual(params["list2"], ["1,0,3"]) + self.assertIn(params["unicode_list"], [["1,2,\xc3\xbcmlaut"], ["1,2,\xfcmlaut"]]) self.assertEqual(params["boolean"], ["1"]) self.assertIn(params["unicode_"], [["\xc3\xbcmlaut"], ["\xfcmlaut"]]) diff --git a/trovebox/http.py b/trovebox/http.py index 0a99fab..6021dcc 100644 --- a/trovebox/http.py +++ b/trovebox/http.py @@ -193,37 +193,45 @@ def _construct_url(self, endpoint): endpoint = "/v%d%s" % (self.config["api_version"], endpoint) return urlunparse((scheme, host, endpoint, '', '', '')) - @staticmethod - def _process_params(params): + def _process_params(self, params): """ Converts Unicode/lists/booleans inside HTTP parameters """ processed_params = {} for key, value in params.items(): - # Extract IDs from objects - if isinstance(value, TroveboxObject): - value = value.id - - # Ensure value is UTF-8 encoded - if isinstance(value, TEXT_TYPE): - value = value.encode("utf-8") - - # Handle lists - if isinstance(value, list): - # Make a copy of the list, to avoid overwriting the original - new_list = list(value) - # Extract IDs from objects in the list - for i, item in enumerate(new_list): - if isinstance(item, TroveboxObject): - new_list[i] = item.id - # Convert list to string - value = ','.join([str(item) for item in new_list]) - - # Handle booleans - if isinstance(value, bool): - value = 1 if value else 0 - processed_params[key] = value + processed_params[key] = self._process_param_value(value) return processed_params + def _process_param_value(self, value): + """ + Returns a UTF-8 string representation of the parameter value, + recursing into lists. + """ + # Extract IDs from objects + if isinstance(value, TroveboxObject): + return str(value.id).encode('utf-8') + + # Ensure strings are UTF-8 encoded + elif isinstance(value, TEXT_TYPE): + return value.encode("utf-8") + + # Handle lists + elif isinstance(value, list): + # Make a copy of the list, to avoid overwriting the original + new_list = list(value) + # Process each item in the list + for i, item in enumerate(new_list): + new_list[i] = self._process_param_value(item) + # new_list elements are UTF-8 encoded strings - simply join up + return b','.join(new_list) + + # Handle booleans + elif isinstance(value, bool): + return b"1" if value else b"0" + + # Unknown - just do our best + else: + return str(value).encode("utf-8") + @staticmethod def _process_response(response): """ From f05878d90fb7894d93d1a7b40952e702e2539161 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Fri, 31 Jan 2014 19:34:01 +0000 Subject: [PATCH 05/10] Fix repr unicode handling Python2 requires utf-8 encoded bytestring Python3 requires unicode string --- tests/unit/test_photos.py | 5 +++++ trovebox/objects/trovebox_object.py | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index 9b10a77..5d77849 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -606,6 +606,11 @@ def test_photo_object_repr_with_id_and_name(self): "name": "Test Name"}) self.assertEqual(repr(photo), "") + def test_photo_object_repr_with_unicode_id(self): + """ Ensure that a unicode id is correctly represented """ + photo = trovebox.objects.photo.Photo(self.client, {"id": "\xfcmlaut"}) + self.assertIn(repr(photo), [b"", ""]) + @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_object_create_attribute(self, _): """ diff --git a/trovebox/objects/trovebox_object.py b/trovebox/objects/trovebox_object.py index 2e02eca..bd52582 100644 --- a/trovebox/objects/trovebox_object.py +++ b/trovebox/objects/trovebox_object.py @@ -1,6 +1,10 @@ """ Base object supporting the storage of custom fields as attributes """ + +from __future__ import unicode_literals +import sys + class TroveboxObject(object): """ Base object supporting the storage of custom fields as attributes """ _type = "None" @@ -41,11 +45,17 @@ def _delete_fields(self): def __repr__(self): if self.name is not None: - return "<%s name='%s'>" % (self.__class__.__name__, self.name) + value = "<%s name='%s'>" % (self.__class__.__name__, self.name) elif self.id is not None: - return "<%s id='%s'>" % (self.__class__.__name__, self.id) + value = "<%s id='%s'>" % (self.__class__.__name__, self.id) else: - return "<%s>" % (self.__class__.__name__) + value = "<%s>" % (self.__class__.__name__) + + # Python2 requires a bytestring + if sys.version < '3': + return value.encode('utf-8') + else: # pragma: no cover + return value def get_fields(self): """ Returns this object's attributes """ From d43c8fb37902becb49b128a44ad351dd86850181 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 1 Feb 2014 19:08:14 +0000 Subject: [PATCH 06/10] Ensure tag update/delete URLs are UTF-8 encoded --- tests/unit/test_tags.py | 24 ++++++++++++++++++++++-- trovebox/api/api_base.py | 10 ++++++++++ trovebox/api/api_tag.py | 9 ++------- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/tests/unit/test_tags.py b/tests/unit/test_tags.py index 61292d4..da81717 100644 --- a/tests/unit/test_tags.py +++ b/tests/unit/test_tags.py @@ -10,13 +10,17 @@ class TestTags(unittest.TestCase): test_host = "test.example.com" test_tags = None - test_tags_dict = [{"count": 11, "id":"tag1"}, - {"count": 5, "id":"tag2"}] + test_tags_dict = [{"count": 11, "id": "tag1"}, + {"count": 5, "id": "tag2"}] + + test_tag_unicode_dict = {"id": "\xfcmlaut"} def setUp(self): self.client = trovebox.Trovebox(host=self.test_host) self.test_tags = [trovebox.objects.tag.Tag(self.client, tag) for tag in self.test_tags_dict] + self.test_tag_unicode = trovebox.objects.tag.Tag(self.client, + self.test_tag_unicode_dict) @staticmethod def _return_value(result, message="", code=200): @@ -89,6 +93,14 @@ def test_tag_object_delete(self, mock_post): self.assertEqual(tag.get_fields(), {}) self.assertEqual(tag.id, None) + @mock.patch.object(trovebox.Trovebox, 'post') + def test_tag_object_delete_unicode(self, mock_post): + """Check that a unicode tag can be deleted using its ID""" + mock_post.return_value = self._return_value(True) + result = self.client.tag.delete(self.test_tag_unicode) + mock_post.assert_called_with("/tag/%C3%BCmlaut/delete.json") + self.assertEqual(result, True) + class TestTagUpdate(TestTags): @mock.patch.object(trovebox.Trovebox, 'post') def test_tag_update(self, mock_post): @@ -118,3 +130,11 @@ def test_tag_object_update(self, mock_post): self.assertEqual(tag.id, "tag2") self.assertEqual(tag.count, 5) + @mock.patch.object(trovebox.Trovebox, 'post') + def test_tag_object_update_unicode(self, mock_post): + """Check that a unicode tag can be updated using its ID""" + mock_post.return_value = self._return_value(self.test_tag_unicode_dict) + result = self.client.tag.update(self.test_tag_unicode, name="Test") + mock_post.assert_called_with("/tag/%C3%BCmlaut/update.json", name="Test") + self.assertEqual(result.id, "\xfcmlaut") + diff --git a/trovebox/api/api_base.py b/trovebox/api/api_base.py index 5aaf16c..52b06e8 100644 --- a/trovebox/api/api_base.py +++ b/trovebox/api/api_base.py @@ -1,6 +1,11 @@ """ api_base.py: Base class for all API classes """ +try: + from urllib.parse import quote # Python3 +except ImportError: + from urllib import quote # Python2 + class ApiBase(object): """ Base class for all API objects """ @@ -27,6 +32,11 @@ def _extract_id(obj): except AttributeError: return obj + @staticmethod + def _quote_url(string): + """ Make a string suitable for insertion into a URL """ + return quote(string.encode('utf-8')) + @staticmethod def _result_to_list(result): """ Handle the case where the result contains no items """ diff --git a/trovebox/api/api_tag.py b/trovebox/api/api_tag.py index 898cffe..eca7916 100644 --- a/trovebox/api/api_tag.py +++ b/trovebox/api/api_tag.py @@ -1,11 +1,6 @@ """ api_tag.py : Trovebox Tag API Classes """ -try: - from urllib.parse import quote # Python3 -except ImportError: - from urllib import quote # Python2 - from trovebox.objects.tag import Tag from .api_base import ApiBase @@ -42,7 +37,7 @@ def delete(self, tag, **kwds): Raises a TroveboxError if not. """ return self._client.post("/tag/%s/delete.json" % - quote(self._extract_id(tag)), + self._quote_url(self._extract_id(tag)), **kwds)["result"] def update(self, tag, **kwds): @@ -53,7 +48,7 @@ def update(self, tag, **kwds): Returns the updated tag object. """ result = self._client.post("/tag/%s/update.json" % - quote(self._extract_id(tag)), + self._quote_url(self._extract_id(tag)), **kwds)["result"] return Tag(self._client, result) From 566f897ace54768e8476f1c958a36ed9568b1c04 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 2 Feb 2014 20:38:11 +0000 Subject: [PATCH 07/10] Add unicode support for the options parameter --- tests/unit/test_activities.py | 8 +++---- tests/unit/test_photos.py | 44 +++++++++++++++++------------------ trovebox/api/api_base.py | 5 ++-- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/tests/unit/test_activities.py b/tests/unit/test_activities.py index 9d209ae..fed967a 100644 --- a/tests/unit/test_activities.py +++ b/tests/unit/test_activities.py @@ -70,15 +70,15 @@ def test_zero_rows(self, mock_get): @mock.patch.object(trovebox.Trovebox, 'get') def test_options(self, mock_get): - """Check that the activity list optionss are applied properly""" + """Check that the activity list options are applied properly""" mock_get.return_value = self._return_value(self.test_activities_dict) self.client.activities.list(options={"foo": "bar", - "test1": "test2"}, + "test1": "\xfcmlaut"}, foo="bar") # Dict element can be any order self.assertIn(mock_get.call_args[0], - [("/activities/foo-bar/test1-test2/list.json",), - ("/activities/test1-test2/foo-bar/list.json",)]) + [("/activities/foo-bar/test1-%C3%BCmlaut/list.json",), + ("/activities/test1-%C3%BCmlaut/foo-bar/list.json",)]) self.assertEqual(mock_get.call_args[1], {"foo": "bar"}) class TestActivitiesPurge(TestActivities): diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index 9b10a77..6466a9b 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -57,27 +57,27 @@ def test_zero_rows(self, mock_get): @mock.patch.object(trovebox.Trovebox, 'get') def test_options(self, mock_get): - """Check that the activity list options are applied properly""" + """Check that the photo list options are applied properly""" mock_get.return_value = self._return_value(self.test_photos_dict) self.client.photos.list(options={"foo": "bar", - "test1": "test2"}, + "test1": "\xfcmlaut"}, foo="bar") # Dict element can be any order self.assertIn(mock_get.call_args[0], - [("/photos/foo-bar/test1-test2/list.json",), - ("/photos/test1-test2/foo-bar/list.json",)]) + [("/photos/foo-bar/test1-%C3%BCmlaut/list.json",), + ("/photos/test1-%C3%BCmlaut/foo-bar/list.json",)]) self.assertEqual(mock_get.call_args[1], {"foo": "bar"}) class TestPhotosShare(TestPhotos): @mock.patch.object(trovebox.Trovebox, 'post') def test_photos_share(self, mock_post): self.client.photos.share(options={"foo": "bar", - "test1": "test2"}, + "test1": "\xfcmlaut"}, foo="bar") # Dict element can be any order self.assertIn(mock_post.call_args[0], - [("/photos/foo-bar/test1-test2/share.json",), - ("/photos/test1-test2/foo-bar/share.json",)]) + [("/photos/foo-bar/test1-%C3%BCmlaut/share.json",), + ("/photos/test1-%C3%BCmlaut/foo-bar/share.json",)]) self.assertEqual(mock_post.call_args[1], {"foo": "bar"}) class TestPhotosUpdate(TestPhotos): @@ -363,12 +363,12 @@ def test_photo_view(self, mock_get): mock_get.return_value = self._return_value(self.test_photos_dict[1]) result = self.client.photo.view(self.test_photos[0], options={"foo": "bar", - "test1": "test2"}, + "test1": "\xfcmlaut"}, returnSizes="20x20") # Dict elemet can be in any order self.assertIn(mock_get.call_args[0], - [("/photo/1a/foo-bar/test1-test2/view.json",), - ("/photo/1a/test1-test2/foo-bar/view.json",)]) + [("/photo/1a/foo-bar/test1-%C3%BCmlaut/view.json",), + ("/photo/1a/test1-%C3%BCmlaut/foo-bar/view.json",)]) self.assertEqual(mock_get.call_args[1], {"returnSizes": "20x20"}) self.assertEqual(result.get_fields(), self.test_photos_dict[1]) @@ -378,13 +378,13 @@ def test_photo_view_id(self, mock_get): mock_get.return_value = self._return_value(self.test_photos_dict[1]) result = self.client.photo.view("1a", options={"foo": "bar", - "test1": "test2"}, + "test1": "\xfcmlaut"}, returnSizes="20x20") # Dict elemet can be in any order self.assertIn(mock_get.call_args[0], - [("/photo/1a/foo-bar/test1-test2/view.json",), - ("/photo/1a/test1-test2/foo-bar/view.json",)]) + [("/photo/1a/foo-bar/test1-%C3%BCmlaut/view.json",), + ("/photo/1a/test1-%C3%BCmlaut/foo-bar/view.json",)]) self.assertEqual(mock_get.call_args[1], {"returnSizes": "20x20"}) self.assertEqual(result.get_fields(), self.test_photos_dict[1]) @@ -455,12 +455,12 @@ def test_photo_next_previous(self, mock_get): "previous": [self.test_photos_dict[1]]}) result = self.client.photo.next_previous(self.test_photos[0], options={"foo": "bar", - "test1": "test2"}, + "test1": "\xfcmlaut"}, foo="bar") # Dict elemet can be in any order self.assertIn(mock_get.call_args[0], - [("/photo/1a/nextprevious/foo-bar/test1-test2.json",), - ("/photo/1a/nextprevious/test1-test2/foo-bar.json",)]) + [("/photo/1a/nextprevious/foo-bar/test1-%C3%BCmlaut.json",), + ("/photo/1a/nextprevious/test1-%C3%BCmlaut/foo-bar.json",)]) self.assertEqual(mock_get.call_args[1], {"foo": "bar"}) self.assertEqual(result["next"][0].get_fields(), self.test_photos_dict[0]) @@ -478,12 +478,12 @@ def test_photo_next_previous_id(self, mock_get): "previous": [self.test_photos_dict[1]]}) result = self.client.photo.next_previous("1a", options={"foo": "bar", - "test1": "test2"}, + "test1": "\xfcmlaut"}, foo="bar") # Dict elemet can be in any order self.assertIn(mock_get.call_args[0], - [("/photo/1a/nextprevious/foo-bar/test1-test2.json",), - ("/photo/1a/nextprevious/test1-test2/foo-bar.json",)]) + [("/photo/1a/nextprevious/foo-bar/test1-%C3%BCmlaut.json",), + ("/photo/1a/nextprevious/test1-%C3%BCmlaut/foo-bar.json",)]) self.assertEqual(mock_get.call_args[1], {"foo": "bar"}) self.assertEqual(result["next"][0].get_fields(), self.test_photos_dict[0]) @@ -500,12 +500,12 @@ def test_photo_object_next_previous(self, mock_get): {"next": [self.test_photos_dict[0]], "previous": [self.test_photos_dict[1]]}) result = self.test_photos[0].next_previous(options={"foo": "bar", - "test1": "test2"}, + "test1": "\xfcmlaut"}, foo="bar") # Dict elemet can be in any order self.assertIn(mock_get.call_args[0], - [("/photo/1a/nextprevious/foo-bar/test1-test2.json",), - ("/photo/1a/nextprevious/test1-test2/foo-bar.json",)]) + [("/photo/1a/nextprevious/foo-bar/test1-%C3%BCmlaut.json",), + ("/photo/1a/nextprevious/test1-%C3%BCmlaut/foo-bar.json",)]) self.assertEqual(mock_get.call_args[1], {"foo": "bar"}) self.assertEqual(result["next"][0].get_fields(), self.test_photos_dict[0]) diff --git a/trovebox/api/api_base.py b/trovebox/api/api_base.py index 52b06e8..ef62607 100644 --- a/trovebox/api/api_base.py +++ b/trovebox/api/api_base.py @@ -12,8 +12,7 @@ class ApiBase(object): def __init__(self, client): self._client = client - @staticmethod - def _build_option_string(options): + def _build_option_string(self, options): """ :param options: dictionary containing the options :returns: option_string formatted for an API endpoint @@ -22,7 +21,7 @@ def _build_option_string(options): if options is not None: for key in options: option_string += "/%s-%s" % (key, options[key]) - return option_string + return self._quote_url(option_string) @staticmethod def _extract_id(obj): From 2890cd5d7d8047dfc356a0ea333cd3fda0cca656 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 2 Feb 2014 20:43:49 +0000 Subject: [PATCH 08/10] Run unit tests under PyPy too --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 7961499..a586d64 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26, py27, py33, coverage +envlist = py26, py27, py33, pypy, coverage [testenv] commands = python -m unittest discover tests/unit From d91601f67b75b8bd600b8bb9f44849575388e2a6 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 2 Feb 2014 21:04:46 +0000 Subject: [PATCH 09/10] Updated pylint ignores --- trovebox/.pylint-disable.patch | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/trovebox/.pylint-disable.patch b/trovebox/.pylint-disable.patch index 5b96583..ee91b6b 100644 --- a/trovebox/.pylint-disable.patch +++ b/trovebox/.pylint-disable.patch @@ -25,9 +25,15 @@ diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_al diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_base.py patched/api/api_base.py --- original/api/api_base.py +++ patched/api/api_base.py -@@ -2,7 +2,7 @@ +@@ -2,12 +2,12 @@ api_base.py: Base class for all API classes """ + try: +- from urllib.parse import quote # Python3 ++ from urllib.parse import quote # Python3 # pylint: disable=import-error,no-name-in-module + except ImportError: + from urllib import quote # Python2 + -class ApiBase(object): +class ApiBase(object): # pylint: disable=too-few-public-methods @@ -37,15 +43,7 @@ diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_ba diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_tag.py patched/api/api_tag.py --- original/api/api_tag.py +++ patched/api/api_tag.py -@@ -2,14 +2,14 @@ - api_tag.py : Trovebox Tag API Classes - """ - try: -- from urllib.parse import quote # Python3 -+ from urllib.parse import quote # Python3 # pylint: disable=import-error,no-name-in-module - except ImportError: - from urllib import quote # Python2 - +@@ -4,7 +4,7 @@ from trovebox.objects.tag import Tag from .api_base import ApiBase @@ -54,21 +52,18 @@ diff --unified --recursive '--exclude=.pylint-disable.patch' original/api/api_ta """ Definitions of /tags/ API endpoints """ def list(self, **kwds): """ +Only in patched/api: api_tag.py.~5~ diff --unified --recursive '--exclude=.pylint-disable.patch' original/auth.py patched/auth.py --- original/auth.py +++ patched/auth.py -@@ -4,7 +4,7 @@ - from __future__ import unicode_literals +@@ -5,13 +5,13 @@ import os + import io try: - from configparser import ConfigParser # Python3 + from configparser import ConfigParser # Python3 # pylint: disable=import-error except ImportError: from ConfigParser import SafeConfigParser as ConfigParser # Python2 - try: -@@ -12,9 +12,9 @@ - except ImportError: # pragma: no cover - import StringIO as io # Python2 -class Auth(object): +class Auth(object): # pylint: disable=too-few-public-methods @@ -78,7 +73,7 @@ diff --unified --recursive '--exclude=.pylint-disable.patch' original/auth.py pa consumer_key, consumer_secret, token, token_secret): if host is None: -@@ -69,7 +69,7 @@ +@@ -66,7 +66,7 @@ parser = ConfigParser() parser.optionxform = str # Case-sensitive options try: @@ -170,7 +165,7 @@ diff --unified --recursive '--exclude=.pylint-disable.patch' original/main.py pa diff --unified --recursive '--exclude=.pylint-disable.patch' original/objects/trovebox_object.py patched/objects/trovebox_object.py --- original/objects/trovebox_object.py +++ patched/objects/trovebox_object.py -@@ -5,7 +5,7 @@ +@@ -9,7 +9,7 @@ """ Base object supporting the storage of custom fields as attributes """ _type = "None" def __init__(self, client, json_dict): From ab7d89cad8a45e09eae197d0081c55cc6911c159 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 2 Feb 2014 21:10:55 +0000 Subject: [PATCH 10/10] Bump to v0.6.1 --- CHANGELOG | 8 ++++++++ trovebox/_version.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 9e2c111..f9e14c8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,14 @@ Trovebox Python Library Changelog ================================= +v0.6.2 +====== + * Support Unicode tags (#74, #77) + * Ensure lists inside parameters are UTF-8 encoded (#74, #77) + * Fix repr unicode handling (#75) + * Support unicode filenames (#72, #73) + * Add Pypy to unit testing list (#78) + v0.6.1 ====== * Perform user expansion when uploading files from the CLI (#59, #70) diff --git a/trovebox/_version.py b/trovebox/_version.py index 435c700..57b310b 100644 --- a/trovebox/_version.py +++ b/trovebox/_version.py @@ -1,2 +1,2 @@ """Current version string""" -__version__ = "0.6.1" +__version__ = "0.6.2"