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/tests/data/test_ünicode_photo.jpg b/tests/data/test_ünicode_photo.jpg new file mode 100644 index 0000000..bc469e3 Binary files /dev/null and b/tests/data/test_ünicode_photo.jpg differ 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 @@ class TestPhotos(test_base.TestBase): 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/ünicode_test_file.txt b/tests/unit/data/ünicode_test_file.txt new file mode 100644 index 0000000..4fff881 --- /dev/null +++ b/tests/unit/data/ünicode_test_file.txt @@ -0,0 +1 @@ +Test File 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 @@ class TestActivitiesList(TestActivities): @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_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 @@ class TestCli(unittest.TestCase): 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): 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 @@ class TestHttp(unittest.TestCase): 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 @@ class TestHttp(unittest.TestCase): 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/tests/unit/test_photos.py b/tests/unit/test_photos.py index 9b10a77..3111fa0 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -57,27 +57,27 @@ class TestPhotosList(TestPhotos): @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 @@ class TestPhotoView(TestPhotos): 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 @@ class TestPhotoView(TestPhotos): 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]) @@ -419,7 +419,7 @@ class TestPhotoUpload(TestPhotos): 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): @@ -455,12 +455,12 @@ class TestPhotoNextPrevious(TestPhotos): "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 @@ class TestPhotoNextPrevious(TestPhotos): "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 @@ class TestPhotoNextPrevious(TestPhotos): {"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]) @@ -606,6 +606,11 @@ class TestPhotoObject(TestPhotos): "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/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 @@ import trovebox 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 @@ class TestTagDelete(TestTags): 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 @@ class TestTagUpdate(TestTags): 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/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 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): 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" diff --git a/trovebox/api/api_base.py b/trovebox/api/api_base.py index 5aaf16c..ef62607 100644 --- a/trovebox/api/api_base.py +++ b/trovebox/api/api_base.py @@ -1,14 +1,18 @@ """ 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 """ 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 @@ -17,7 +21,7 @@ class ApiBase(object): 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): @@ -27,6 +31,11 @@ class ApiBase(object): 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 @@ class ApiTag(ApiBase): 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 @@ class ApiTag(ApiBase): 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) 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 @@ class Http(object): 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): """ 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] 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 @@ class TroveboxObject(object): 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 """