Merge branch 'release-0.6.2'

This commit is contained in:
sneakypete81 2014-02-02 21:19:42 +00:00
commit 209a1da27c
17 changed files with 212 additions and 90 deletions

View file

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -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

View file

@ -0,0 +1 @@
Test File

View file

@ -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):

View file

@ -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):

View file

@ -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"]])

View file

@ -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), "<Photo name='Test Name'>")
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"<Photo id='\xc3\xbcmlaut'>", "<Photo id='\xfcmlaut'>"])
@mock.patch.object(trovebox.Trovebox, 'post')
def test_photo_object_create_attribute(self, _):
"""

View file

@ -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")

View file

@ -1,5 +1,5 @@
[tox]
envlist = py26, py27, py33, coverage
envlist = py26, py27, py33, pypy, coverage
[testenv]
commands = python -m unittest discover tests/unit

View file

@ -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):

View file

@ -1,2 +1,2 @@
"""Current version string"""
__version__ = "0.6.1"
__version__ = "0.6.2"

View file

@ -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 """

View file

@ -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)

View file

@ -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):
"""

View file

@ -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]

View file

@ -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 """