From 24fcf3f41554429247fe705121bc0ca68bdb71c7 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 15 Jun 2013 18:40:38 +0100 Subject: [PATCH 01/58] Added basic get/post unit tests --- tests/unit/__init__.py | 0 tests/unit/test_http.py | 108 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/test_http.py diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py new file mode 100644 index 0000000..7fd18d2 --- /dev/null +++ b/tests/unit/test_http.py @@ -0,0 +1,108 @@ +from __future__ import unicode_literals +import json +import httpretty +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + +import openphoto + +class TestHttp(unittest.TestCase): + TEST_HOST = "test.example.com" + TEST_ENDPOINT = "test.json" + TEST_URI = "http://%s/%s" % (TEST_HOST, TEST_ENDPOINT) + TEST_DATA = {"message": "Test Message", + "code": 200, + "result": "Test Result"} + + def setUp(self): + self.client = openphoto.OpenPhoto(host=self.TEST_HOST, + consumer_key="dummy", + consumer_secret="dummy", + token="dummy", + token_secret="dummy") + + def _register_uri(self, method, uri=TEST_URI, data=TEST_DATA, **kwds): + """Convenience wrapper around httpretty.register_uri""" + body = json.dumps(data) + httpretty.register_uri(method, uri=uri, body=body, **kwds) + + @staticmethod + def _last_request(): + """This is a temporary measure until httpretty PR#59 is merged""" + return httpretty.httpretty.last_request + + def test_attributes(self): + self.assertEqual(self.client.host, self.TEST_HOST) + self.assertEqual(self.client.config.host, self.TEST_HOST) + + @httpretty.activate + def test_get_with_parameters(self): + self._register_uri(httpretty.GET) + response = self.client.get(self.TEST_ENDPOINT, + foo="bar", spam="eggs") + self.assertIn("OAuth", self._last_request().headers["authorization"]) + self.assertEqual(self._last_request().querystring["foo"], ["bar"]) + self.assertEqual(self._last_request().querystring["spam"], ["eggs"]) + self.assertEqual(response, self.TEST_DATA) + self.assertEqual(self.client.last_url, self.TEST_URI) + self.assertEqual(self.client.last_params, {"foo": "bar", "spam": "eggs"}) + self.assertEqual(self.client.last_response.json(), self.TEST_DATA) + + @httpretty.activate + def test_post_with_parameters(self): + self._register_uri(httpretty.POST) + response = self.client.post(self.TEST_ENDPOINT, + foo="bar", spam="eggs") + self.assertEqual(self._last_request().body, "foo=bar&spam=eggs") + self.assertEqual(response, self.TEST_DATA) + self.assertEqual(self.client.last_url, self.TEST_URI) + self.assertEqual(self.client.last_params, {"foo": "bar", "spam": "eggs"}) + self.assertEqual(self.client.last_response.json(), self.TEST_DATA) + + @httpretty.activate + def test_get_without_oauth(self): + self.client = openphoto.OpenPhoto(host=self.TEST_HOST) + self._register_uri(httpretty.GET) + response = self.client.get(self.TEST_ENDPOINT) + self.assertNotIn("authorization", self._last_request().headers) + self.assertEqual(response, self.TEST_DATA) + + @httpretty.activate + def test_post_without_oauth_raises_exception(self): + self.client = openphoto.OpenPhoto(host=self.TEST_HOST) + self._register_uri(httpretty.POST) + with self.assertRaises(openphoto.OpenPhotoError): + self.client.post(self.TEST_ENDPOINT) + + @httpretty.activate + def test_get_without_response_processing(self): + self._register_uri(httpretty.GET) + response = self.client.get(self.TEST_ENDPOINT, process_response=False) + self.assertEqual(response, json.dumps(self.TEST_DATA)) + + @httpretty.activate + def test_post_without_response_processing(self): + self._register_uri(httpretty.POST) + response = self.client.post(self.TEST_ENDPOINT, process_response=False) + self.assertEqual(response, json.dumps(self.TEST_DATA)) + + @httpretty.activate + def test_get_parameter_processing(self): + self._register_uri(httpretty.GET) + photo = openphoto.objects.Photo(None, {"id": "photo_id"}) + album = openphoto.objects.Album(None, {"id": "album_id"}) + tag = openphoto.objects.Tag(None, {"id": "tag_id"}) + response = self.client.get(self.TEST_ENDPOINT, + photo=photo, album=album, tag=tag, + list_=[photo, album, tag], + boolean=True, + unicode_="\xfcmlaut") + params=self._last_request().querystring + self.assertEqual(params["photo"], ["photo_id"]) + 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["boolean"], ["1"]) + self.assertEqual(params["unicode_"], ["\xc3\xbcmlaut"]) From b9c947c94cd9dfb844bee0380c2e1a450e2bf46e Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 16 Jun 2013 12:09:24 +0100 Subject: [PATCH 02/58] Added more unit tests File post, API version selection, HTTP errors --- tests/unit/data/test_file.txt | 1 + tests/unit/test_http.py | 68 +++++++++++++++---- tests/unit/test_http_errors.py | 120 +++++++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 tests/unit/data/test_file.txt create mode 100644 tests/unit/test_http_errors.py diff --git a/tests/unit/data/test_file.txt b/tests/unit/data/test_file.txt new file mode 100644 index 0000000..4fff881 --- /dev/null +++ b/tests/unit/data/test_file.txt @@ -0,0 +1 @@ +Test File diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py index 7fd18d2..971dd90 100644 --- a/tests/unit/test_http.py +++ b/tests/unit/test_http.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +import os import json import httpretty try: @@ -15,17 +16,21 @@ class TestHttp(unittest.TestCase): TEST_DATA = {"message": "Test Message", "code": 200, "result": "Test Result"} + TEST_OAUTH = {"consumer_key": "dummy", + "consumer_secret": "dummy", + "token": "dummy", + "token_secret": "dummy"} + TEST_FILE = os.path.join("tests", "unit", "data", "test_file.txt") + def setUp(self): - self.client = openphoto.OpenPhoto(host=self.TEST_HOST, - consumer_key="dummy", - consumer_secret="dummy", - token="dummy", - token_secret="dummy") + self.client = openphoto.OpenPhoto(host=self.TEST_HOST, **self.TEST_OAUTH) - def _register_uri(self, method, uri=TEST_URI, data=TEST_DATA, **kwds): + def _register_uri(self, method, uri=TEST_URI, data=TEST_DATA, body=None, + **kwds): """Convenience wrapper around httpretty.register_uri""" - body = json.dumps(data) + if body is None: + body = json.dumps(data) httpretty.register_uri(method, uri=uri, body=body, **kwds) @staticmethod @@ -94,11 +99,11 @@ class TestHttp(unittest.TestCase): photo = openphoto.objects.Photo(None, {"id": "photo_id"}) album = openphoto.objects.Album(None, {"id": "album_id"}) tag = openphoto.objects.Tag(None, {"id": "tag_id"}) - response = self.client.get(self.TEST_ENDPOINT, - photo=photo, album=album, tag=tag, - list_=[photo, album, tag], - boolean=True, - unicode_="\xfcmlaut") + self.client.get(self.TEST_ENDPOINT, + photo=photo, album=album, tag=tag, + list_=[photo, album, tag], + boolean=True, + unicode_="\xfcmlaut") params=self._last_request().querystring self.assertEqual(params["photo"], ["photo_id"]) self.assertEqual(params["album"], ["album_id"]) @@ -106,3 +111,42 @@ class TestHttp(unittest.TestCase): self.assertEqual(params["list_"], ["photo_id,album_id,tag_id"]) self.assertEqual(params["boolean"], ["1"]) self.assertEqual(params["unicode_"], ["\xc3\xbcmlaut"]) + + @httpretty.activate + def test_get_with_api_version(self): + self.client = openphoto.OpenPhoto(host=self.TEST_HOST, api_version=1) + self._register_uri(httpretty.GET, + uri="http://%s/v1/%s" % (self.TEST_HOST, + self.TEST_ENDPOINT)) + self.client.get(self.TEST_ENDPOINT) + + @httpretty.activate + def test_post_with_api_version(self): + self.client = openphoto.OpenPhoto(host=self.TEST_HOST, api_version=1, + **self.TEST_OAUTH) + self._register_uri(httpretty.POST, + uri="http://%s/v1/%s" % (self.TEST_HOST, + self.TEST_ENDPOINT)) + self.client.post(self.TEST_ENDPOINT) + + @httpretty.activate + def test_post_file(self): + self._register_uri(httpretty.POST) + with open(self.TEST_FILE, 'rb') as in_file: + response = self.client.post(self.TEST_ENDPOINT, + files={"file": in_file}) + self.assertEqual(response, self.TEST_DATA) + body = self._last_request().body + self.assertIn("Content-Disposition: form-data; "+ + "name=\"file\"; filename=\"test_file.txt\"", body) + self.assertIn("Test File", body) + + + @httpretty.activate + def test_post_file_parameters_are_sent_as_querystring(self): + self._register_uri(httpretty.POST) + with open(self.TEST_FILE, 'rb') as in_file: + response = self.client.post(self.TEST_ENDPOINT, foo="bar", + files={"file": in_file}) + self.assertEqual(response, self.TEST_DATA) + self.assertEqual(self._last_request().querystring["foo"], ["bar"]) diff --git a/tests/unit/test_http_errors.py b/tests/unit/test_http_errors.py new file mode 100644 index 0000000..80eec6d --- /dev/null +++ b/tests/unit/test_http_errors.py @@ -0,0 +1,120 @@ +from __future__ import unicode_literals +import json +import httpretty +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + +import openphoto +from tests.unit.test_http import TestHttp + +class TestHttpErrors(TestHttp): + def _register_uri(self, method, uri=TestHttp.TEST_URI, + data=None, body=None, status=200, **kwds): + """Convenience wrapper around httpretty.register_uri""" + if data is None: + data = self.TEST_DATA + # Set the JSON return code to match the HTTP status + data["code"] = status + if body is None: + body = json.dumps(data) + httpretty.register_uri(method, uri=uri, body=body, status=status, + **kwds) + + @httpretty.activate + def test_get_with_error_status_raises_openphoto_exception(self): + self._register_uri(httpretty.GET, status=500) + with self.assertRaises(openphoto.OpenPhotoError): + self.client.get(self.TEST_ENDPOINT) + + @httpretty.activate + def test_post_with_error_status_raises_openphoto_exception(self): + self._register_uri(httpretty.POST, status=500) + with self.assertRaises(openphoto.OpenPhotoError): + self.client.post(self.TEST_ENDPOINT) + + # TODO: 404 status should raise 404 error, even if JSON is valid + @unittest.expectedFailure + @httpretty.activate + def test_get_with_404_status_raises_404_exception(self): + self._register_uri(httpretty.GET, status=404) + with self.assertRaises(openphoto.OpenPhoto404Error): + response = self.client.get(self.TEST_ENDPOINT) + + # TODO: 404 status should raise 404 error, even if JSON is valid + @unittest.expectedFailure + @httpretty.activate + def test_post_with_404_status_raises_404_exception(self): + self._register_uri(httpretty.POST, status=404) + with self.assertRaises(openphoto.OpenPhoto404Error): + response = self.client.post(self.TEST_ENDPOINT) + + @httpretty.activate + def test_get_with_invalid_json_raises_exception(self): + self._register_uri(httpretty.GET, body="Invalid JSON") + with self.assertRaises(ValueError): + self.client.get(self.TEST_ENDPOINT) + + @httpretty.activate + def test_post_with_invalid_json_raises_exception(self): + self._register_uri(httpretty.POST, body="Invalid JSON") + with self.assertRaises(ValueError): + self.client.post(self.TEST_ENDPOINT) + + @httpretty.activate + def test_get_with_error_status_and_invalid_json_raises_openphoto_exception(self): + self._register_uri(httpretty.GET, body="Invalid JSON", status=500) + with self.assertRaises(openphoto.OpenPhotoError): + response = self.client.get(self.TEST_ENDPOINT) + + @httpretty.activate + def test_post_with_error_status_and_invalid_json_raises_openphoto_exception(self): + self._register_uri(httpretty.POST, body="Invalid JSON", status=500) + with self.assertRaises(openphoto.OpenPhotoError): + response = self.client.post(self.TEST_ENDPOINT) + + @httpretty.activate + def test_get_with_404_status_and_invalid_json_raises_404_exception(self): + self._register_uri(httpretty.GET, body="Invalid JSON", status=404) + with self.assertRaises(openphoto.OpenPhoto404Error): + response = self.client.get(self.TEST_ENDPOINT) + + @httpretty.activate + def test_post_with_404_status_and_invalid_json_raises_404_exception(self): + self._register_uri(httpretty.POST, body="Invalid JSON", status=404) + with self.assertRaises(openphoto.OpenPhoto404Error): + response = self.client.post(self.TEST_ENDPOINT) + + @httpretty.activate + def test_get_with_duplicate_status_raises_duplicate_exception(self): + data = {"message": "This photo already exists", "code": 409} + self._register_uri(httpretty.GET, data=data, status=409) + with self.assertRaises(openphoto.OpenPhotoDuplicateError): + response = self.client.get(self.TEST_ENDPOINT) + + @httpretty.activate + def test_post_with_duplicate_status_raises_duplicate_exception(self): + data = {"message": "This photo already exists", "code": 409} + self._register_uri(httpretty.POST, data=data, status=409) + with self.assertRaises(openphoto.OpenPhotoDuplicateError): + response = self.client.post(self.TEST_ENDPOINT) + + # TODO: Status code mismatch should raise an exception + @unittest.expectedFailure + @httpretty.activate + def test_get_with_status_code_mismatch_raises_openphoto_exception(self): + data = {"message": "Test Message", "code": 200} + self._register_uri(httpretty.GET, data=data, status=202) + with self.assertRaises(openphoto.OpenPhotoError): + response = self.client.get(self.TEST_ENDPOINT) + + # TODO: Status code mismatch should raise an exception + @unittest.expectedFailure + @httpretty.activate + def test_post_with_status_code_mismatch_raises_openphoto_exception(self): + data = {"message": "Test Message", "code": 200} + self._register_uri(httpretty.POST, data=data, status=202) + with self.assertRaises(openphoto.OpenPhotoError): + response = self.client.post(self.TEST_ENDPOINT) + From c1c309bc9d7d605c65bb0cbeddd44874bf04e87c Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 16 Jun 2013 13:09:31 +0100 Subject: [PATCH 03/58] Started photo API unit tests --- tests/unit/test_photos.py | 104 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/unit/test_photos.py diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py new file mode 100644 index 0000000..bf87ab6 --- /dev/null +++ b/tests/unit/test_photos.py @@ -0,0 +1,104 @@ +from __future__ import unicode_literals +import mock +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + +import openphoto + +class TestPhotos(unittest.TestCase): + TEST_HOST = "test.example.com" + + TEST_PHOTOS = [{"id": "1a", + "tags": ["tag1", "tag2"], + "totalPages": 1, + "totalRows": 2}, + {"id": "2b", + "tags": ["tag3", "tag4"], + "totalPages": 1, + "totalRows": 2}] + + def setUp(self): + self.client = openphoto.OpenPhoto(host=self.TEST_HOST) + + @staticmethod + def _return_value(result, message="", code=200): + return {"message": message, "code": code, "result": result} + +class TestPhotosList(TestPhotos): + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_list(self, mock_get): + photos = self.TEST_PHOTOS + mock_get.return_value = self._return_value(photos) + + result = self.client.photos.list() + mock_get.assert_called_with("/photos/list.json") + self.assertEqual(len(result), 2) + self.assertEqual(result[0].id, "1a") + self.assertEqual(result[0].tags, ["tag1", "tag2"]) + self.assertEqual(result[1].id, "2b") + self.assertEqual(result[1].tags, ["tag3", "tag4"]) + +class TestPhotosUpdate(TestPhotos): + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_update(self, mock_post): + mock_post.return_value = self._return_value(True) + result = self.client.photos.update(["1a", "2b"], title="Test") + mock_post.assert_called_with("/photos/update.json", + ids=["1a", "2b"], title="Test") + self.assertEqual(result, True) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_update_failure_raises_exception(self, mock_post): + mock_post.return_value = self._return_value(False) + with self.assertRaises(openphoto.OpenPhotoError): + self.client.photos.update(["1a", "2b"], title="Test") + +class TestPhotosDelete(TestPhotos): + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_delete(self, mock_post): + mock_post.return_value = self._return_value(True) + result = self.client.photos.delete(["1a", "2b"]) + mock_post.assert_called_with("/photos/delete.json", ids=["1a", "2b"]) + self.assertEqual(result, True) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_delete_failure_raises_exception(self, mock_post): + mock_post.return_value = self._return_value(False) + with self.assertRaises(openphoto.OpenPhotoError): + self.client.photos.delete(["1a", "2b"]) + +class TestPhotoDelete(TestPhotos): + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_delete(self, mock_post): + mock_post.return_value = self._return_value(True) + result = self.client.photo.delete("1a") + mock_post.assert_called_with("/photo/1a/delete.json") + self.assertEqual(result, True) + + # TODO: photo.delete should raise exception on failure + @unittest.expectedFailure + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_delete_failure_raises_exception(self, mock_post): + mock_post.return_value = self._return_value(False) + with self.assertRaises(openphoto.OpenPhotoError): + self.client.photo.delete("1a") + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_object_delete(self, mock_post): + mock_post.return_value = self._return_value(True) + photo = openphoto.objects.Photo(self.client, self.TEST_PHOTOS[0]) + result = photo.delete() + mock_post.assert_called_with("/photo/1a/delete.json") + self.assertEqual(result, True) + + # TODO: photo.delete should raise exception on failure + @unittest.expectedFailure + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_object_delete_failure_raises_exception(self, mock_post): + mock_post.return_value = self._return_value(False) + photo = openphoto.objects.Photo(self.client, self.TEST_PHOTOS[0]) + with self.assertRaises(openphoto.OpenPhotoError): + photo.delete() + From 98abec56979d44d518be491da6d73fbc9a876394 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 16 Jun 2013 19:39:55 +0100 Subject: [PATCH 04/58] Additional photo API unit tests --- tests/unit/test_photos.py | 353 +++++++++++++++++++++++++++++++++----- 1 file changed, 306 insertions(+), 47 deletions(-) diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index bf87ab6..a942028 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -1,4 +1,6 @@ from __future__ import unicode_literals +import os +import base64 import mock try: import unittest2 as unittest # Python2.6 @@ -9,18 +11,15 @@ import openphoto class TestPhotos(unittest.TestCase): TEST_HOST = "test.example.com" - - TEST_PHOTOS = [{"id": "1a", - "tags": ["tag1", "tag2"], - "totalPages": 1, - "totalRows": 2}, - {"id": "2b", - "tags": ["tag3", "tag4"], - "totalPages": 1, - "totalRows": 2}] - + TEST_FILE = os.path.join("tests", "unit", "data", "test_file.txt") + TEST_PHOTOS_DICT = [{"id": "1a", "tags": ["tag1", "tag2"], + "totalPages": 1, "totalRows": 2}, + {"id": "2b", "tags": ["tag3", "tag4"], + "totalPages": 1, "totalRows": 2}] def setUp(self): self.client = openphoto.OpenPhoto(host=self.TEST_HOST) + self.TEST_PHOTOS = [openphoto.objects.Photo(self.client, photo) + for photo in self.TEST_PHOTOS_DICT] @staticmethod def _return_value(result, message="", code=200): @@ -28,12 +27,11 @@ class TestPhotos(unittest.TestCase): class TestPhotosList(TestPhotos): @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_list(self, mock_get): - photos = self.TEST_PHOTOS - mock_get.return_value = self._return_value(photos) + def test_photos_list(self, mock): + mock.return_value = self._return_value(self.TEST_PHOTOS_DICT) result = self.client.photos.list() - mock_get.assert_called_with("/photos/list.json") + mock.assert_called_with("/photos/list.json") self.assertEqual(len(result), 2) self.assertEqual(result[0].id, "1a") self.assertEqual(result[0].tags, ["tag1", "tag2"]) @@ -41,64 +39,325 @@ class TestPhotosList(TestPhotos): self.assertEqual(result[1].tags, ["tag3", "tag4"]) class TestPhotosUpdate(TestPhotos): + # TODO: photos.update should accept a list of Photo objects + @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_update(self, mock_post): - mock_post.return_value = self._return_value(True) - result = self.client.photos.update(["1a", "2b"], title="Test") - mock_post.assert_called_with("/photos/update.json", + def test_photos_update(self, mock): + mock.return_value = self._return_value(True) + result = self.client.photos.update(self.TEST_PHOTOS, title="Test") + mock.assert_called_with("/photos/update.json", ids=["1a", "2b"], title="Test") self.assertEqual(result, True) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_update_failure_raises_exception(self, mock_post): - mock_post.return_value = self._return_value(False) - with self.assertRaises(openphoto.OpenPhotoError): - self.client.photos.update(["1a", "2b"], title="Test") - -class TestPhotosDelete(TestPhotos): - @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_delete(self, mock_post): - mock_post.return_value = self._return_value(True) - result = self.client.photos.delete(["1a", "2b"]) - mock_post.assert_called_with("/photos/delete.json", ids=["1a", "2b"]) + def test_photos_update_ids(self, mock): + mock.return_value = self._return_value(True) + result = self.client.photos.update(["1a", "2b"], title="Test") + mock.assert_called_with("/photos/update.json", + ids=["1a", "2b"], title="Test") self.assertEqual(result, True) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_delete_failure_raises_exception(self, mock_post): - mock_post.return_value = self._return_value(False) + def test_photos_update_failure_raises_exception(self, mock): + mock.return_value = self._return_value(False) with self.assertRaises(openphoto.OpenPhotoError): - self.client.photos.delete(["1a", "2b"]) + self.client.photos.update(self.TEST_PHOTOS, title="Test") + +class TestPhotosDelete(TestPhotos): + # TODO: photos.delete should accept a list of Photo objects + @unittest.expectedFailure + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_photos_delete(self, mock): + mock.return_value = self._return_value(True) + result = self.client.photos.delete(self.TEST_PHOTOS) + mock.assert_called_with("/photos/delete.json", ids=["1a", "2b"]) + self.assertEqual(result, True) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_photos_delete_ids(self, mock): + mock.return_value = self._return_value(True) + result = self.client.photos.delete(["1a", "2b"]) + mock.assert_called_with("/photos/delete.json", ids=["1a", "2b"]) + self.assertEqual(result, True) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_photos_delete_failure_raises_exception(self, mock): + mock.return_value = self._return_value(False) + with self.assertRaises(openphoto.OpenPhotoError): + self.client.photos.delete(self.TEST_PHOTOS) class TestPhotoDelete(TestPhotos): @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_delete(self, mock_post): - mock_post.return_value = self._return_value(True) + def test_photo_delete(self, mock): + mock.return_value = self._return_value(True) + result = self.client.photo.delete(self.TEST_PHOTOS[0]) + mock.assert_called_with("/photo/1a/delete.json") + self.assertEqual(result, True) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_photo_delete_id(self, mock): + mock.return_value = self._return_value(True) result = self.client.photo.delete("1a") - mock_post.assert_called_with("/photo/1a/delete.json") + mock.assert_called_with("/photo/1a/delete.json") self.assertEqual(result, True) # TODO: photo.delete should raise exception on failure @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_delete_failure_raises_exception(self, mock_post): - mock_post.return_value = self._return_value(False) + def test_photo_delete_failure_raises_exception(self, mock): + mock.return_value = self._return_value(False) with self.assertRaises(openphoto.OpenPhotoError): - self.client.photo.delete("1a") + self.client.photo.delete(self.TEST_PHOTOS[0]) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_object_delete(self, mock_post): - mock_post.return_value = self._return_value(True) - photo = openphoto.objects.Photo(self.client, self.TEST_PHOTOS[0]) - result = photo.delete() - mock_post.assert_called_with("/photo/1a/delete.json") + def test_photo_object_delete(self, mock): + mock.return_value = self._return_value(True) + result = self.TEST_PHOTOS[0].delete() + mock.assert_called_with("/photo/1a/delete.json") self.assertEqual(result, True) # TODO: photo.delete should raise exception on failure @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_object_delete_failure_raises_exception(self, mock_post): - mock_post.return_value = self._return_value(False) - photo = openphoto.objects.Photo(self.client, self.TEST_PHOTOS[0]) + def test_photo_object_delete_failure_raises_exception(self, mock): + mock.return_value = self._return_value(False) with self.assertRaises(openphoto.OpenPhotoError): - photo.delete() + self.TEST_PHOTOS[0].delete() +class TestPhotoEdit(TestPhotos): + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_photo_edit(self, mock): + mock.return_value = self._return_value({"markup": "
"}) + result = self.client.photo.edit(self.TEST_PHOTOS[0]) + mock.assert_called_with("/photo/1a/edit.json") + self.assertEqual(result, "") + + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_photo_edit_id(self, mock): + mock.return_value = self._return_value({"markup": ""}) + result = self.client.photo.edit("1a") + mock.assert_called_with("/photo/1a/edit.json") + self.assertEqual(result, "") + + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_photo_object_edit(self, mock): + mock.return_value = self._return_value({"markup": ""}) + result = self.TEST_PHOTOS[0].edit() + mock.assert_called_with("/photo/1a/edit.json") + self.assertEqual(result, "") + +class TestPhotoReplace(TestPhotos): + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_photo_replace(self, mock): + with self.assertRaises(NotImplementedError): + self.client.photo.replace(self.TEST_PHOTOS[0], self.TEST_FILE) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_photo_replace_id(self, mock): + with self.assertRaises(NotImplementedError): + self.client.photo.replace("1a", self.TEST_FILE) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_photo_object_replace(self, mock): + with self.assertRaises(NotImplementedError): + self.TEST_PHOTOS[0].replace(self.TEST_FILE) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_photo_replace_encoded(self, mock): + with self.assertRaises(NotImplementedError): + self.client.photo.replace_encoded(self.TEST_PHOTOS[0], self.TEST_FILE) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_photo_replace_encoded_id(self, mock): + with self.assertRaises(NotImplementedError): + self.client.photo.replace_encoded("1a", self.TEST_FILE) + + # TODO: replace_encoded parameter should be called photo_file, + # not encoded_photo + @unittest.expectedFailure + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_photo_object_replace_encoded(self, mock): + with self.assertRaises(NotImplementedError): + self.TEST_PHOTOS[0].replace_encoded(photo_file=self.TEST_FILE) + +class TestPhotoUpdate(TestPhotos): + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_photo_update(self, mock): + mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) + result = self.client.photo.update(self.TEST_PHOTOS[0], title="Test") + mock.assert_called_with("/photo/1a/update.json", title="Test") + self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[1]) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_photo_update_id(self, mock): + mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) + result = self.client.photo.update("1a", title="Test") + mock.assert_called_with("/photo/1a/update.json", title="Test") + self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[1]) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_photo_object_update(self, mock): + mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) + photo = self.TEST_PHOTOS[0] + photo.update(title="Test") + mock.assert_called_with("/photo/1a/update.json", title="Test") + self.assertEqual(photo.get_fields(), self.TEST_PHOTOS_DICT[1]) + +class TestPhotoView(TestPhotos): + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_photo_view(self, mock): + mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) + result = self.client.photo.view(self.TEST_PHOTOS[0], returnSizes="20x20") + mock.assert_called_with("/photo/1a/view.json", returnSizes="20x20") + self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[1]) + + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_photo_view_id(self, mock): + mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) + result = self.client.photo.view("1a", returnSizes="20x20") + mock.assert_called_with("/photo/1a/view.json", returnSizes="20x20") + self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[1]) + + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_photo_object_view(self, mock): + mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) + photo = self.TEST_PHOTOS[0] + photo.view(returnSizes="20x20") + mock.assert_called_with("/photo/1a/view.json", returnSizes="20x20") + self.assertEqual(photo.get_fields(), self.TEST_PHOTOS_DICT[1]) + +class TestPhotoUpload(TestPhotos): + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_photo_upload(self, mock): + mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[0]) + result = self.client.photo.upload(self.TEST_FILE, title="Test") + # It's not possible to compare the file object, + # so check each parameter individually + endpoint = mock.call_args[0] + title = mock.call_args[1]["title"] + files = mock.call_args[1]["files"] + self.assertEqual(endpoint, ("/photo/upload.json",)) + self.assertEqual(title, "Test") + self.assertIn("photo", files) + self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[0]) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_photo_upload_encoded(self, mock): + encoded_file = base64.b64encode(open(self.TEST_FILE, "rb").read()) + mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[0]) + result = self.client.photo.upload_encoded(self.TEST_FILE, title="Test") + mock.assert_called_with("/photo/upload.json", + photo=encoded_file, title="Test") + self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[0]) + +class TestPhotoDynamicUrl(TestPhotos): + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_photo_dynamic_url(self, mock): + with self.assertRaises(NotImplementedError): + self.client.photo.dynamic_url(self.TEST_PHOTOS[0]) + + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_photo_dynamic_url_id(self, mock): + with self.assertRaises(NotImplementedError): + self.client.photo.dynamic_url("1a") + + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_photo_object_dynamic_url(self, mock): + with self.assertRaises(NotImplementedError): + self.TEST_PHOTOS[0].dynamic_url() + +class TestPhotoNextPrevious(TestPhotos): + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_photo_next_previous(self, mock): + mock.return_value = self._return_value( + {"next": [self.TEST_PHOTOS_DICT[0]], + "previous": [self.TEST_PHOTOS_DICT[1]]}) + result = self.client.photo.next_previous(self.TEST_PHOTOS[0]) + mock.assert_called_with("/photo/1a/nextprevious.json") + self.assertEqual(result["next"][0].get_fields(), + self.TEST_PHOTOS_DICT[0]) + self.assertEqual(result["previous"][0].get_fields(), + self.TEST_PHOTOS_DICT[1]) + + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_photo_next_previous_id(self, mock): + mock.return_value = self._return_value( + {"next": [self.TEST_PHOTOS_DICT[0]], + "previous": [self.TEST_PHOTOS_DICT[1]]}) + result = self.client.photo.next_previous("1a") + mock.assert_called_with("/photo/1a/nextprevious.json") + self.assertEqual(result["next"][0].get_fields(), + self.TEST_PHOTOS_DICT[0]) + self.assertEqual(result["previous"][0].get_fields(), + self.TEST_PHOTOS_DICT[1]) + + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_photo_object_next_previous(self, mock): + mock.return_value = self._return_value( + {"next": [self.TEST_PHOTOS_DICT[0]], + "previous": [self.TEST_PHOTOS_DICT[1]]}) + result = self.TEST_PHOTOS[0].next_previous() + mock.assert_called_with("/photo/1a/nextprevious.json") + self.assertEqual(result["next"][0].get_fields(), + self.TEST_PHOTOS_DICT[0]) + self.assertEqual(result["previous"][0].get_fields(), + self.TEST_PHOTOS_DICT[1]) + + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_photo_next(self, mock): + mock.return_value = self._return_value( + {"next": [self.TEST_PHOTOS_DICT[0]]}) + result = self.client.photo.next_previous(self.TEST_PHOTOS[0]) + mock.assert_called_with("/photo/1a/nextprevious.json") + self.assertEqual(result["next"][0].get_fields(), + self.TEST_PHOTOS_DICT[0]) + self.assertNotIn("previous", result) + + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_photo_previous(self, mock): + mock.return_value = self._return_value( + {"previous": [self.TEST_PHOTOS_DICT[1]]}) + result = self.client.photo.next_previous(self.TEST_PHOTOS[0]) + mock.assert_called_with("/photo/1a/nextprevious.json") + self.assertEqual(result["previous"][0].get_fields(), + self.TEST_PHOTOS_DICT[1]) + self.assertNotIn("next", result) + + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_photo_multiple_next_previous(self, mock): + mock.return_value = self._return_value( + {"next": [self.TEST_PHOTOS_DICT[0], self.TEST_PHOTOS_DICT[0]], + "previous": [self.TEST_PHOTOS_DICT[1], self.TEST_PHOTOS_DICT[1]]}) + result = self.client.photo.next_previous(self.TEST_PHOTOS[0]) + mock.assert_called_with("/photo/1a/nextprevious.json") + self.assertEqual(result["next"][0].get_fields(), + self.TEST_PHOTOS_DICT[0]) + self.assertEqual(result["next"][1].get_fields(), + self.TEST_PHOTOS_DICT[0]) + self.assertEqual(result["previous"][0].get_fields(), + self.TEST_PHOTOS_DICT[1]) + self.assertEqual(result["previous"][1].get_fields(), + self.TEST_PHOTOS_DICT[1]) + +class TestPhotoTransform(TestPhotos): + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_transform(self, mock): + mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) + result = self.client.photo.transform(self.TEST_PHOTOS[0], rotate="90") + mock.assert_called_with("/photo/1a/transform.json", rotate="90") + self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[1]) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_transform_id(self, mock): + mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) + result = self.client.photo.transform("1a", rotate="90") + mock.assert_called_with("/photo/1a/transform.json", rotate="90") + self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[1]) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_object_transform(self, mock): + mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) + photo = self.TEST_PHOTOS[0] + photo.transform(rotate="90") + mock.assert_called_with("/photo/1a/transform.json", rotate="90") + self.assertEqual(photo.get_fields(), self.TEST_PHOTOS_DICT[1]) From 75157a15e167455862630665873380c5c2be3e2e Mon Sep 17 00:00:00 2001 From: Pete Date: Wed, 26 Jun 2013 19:42:20 +0100 Subject: [PATCH 05/58] Add CLI testcases. --- tests/unit/test_cli.py | 95 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 tests/unit/test_cli.py diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py new file mode 100644 index 0000000..4733690 --- /dev/null +++ b/tests/unit/test_cli.py @@ -0,0 +1,95 @@ +import os +import sys +import mock +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + +import openphoto +from openphoto.main import main + +class TestException(Exception): + pass + +def raise_exception(_): + raise TestException() + +class TestCli(unittest.TestCase): + TEST_FILE = os.path.join("tests", "unit", "data", "test_file.txt") + + @mock.patch.object(openphoto.main, "OpenPhoto") + def test_defaults(self, MockOpenPhoto): + get = MockOpenPhoto.return_value.get + main([]) + MockOpenPhoto.assert_called_with(config_file=None) + get.assert_called_with("/photos/list.json", process_response=False) + + @mock.patch.object(openphoto.main, "OpenPhoto") + def test_config(self, MockOpenPhoto): + main(["--config=test"]) + MockOpenPhoto.assert_called_with(config_file="test") + + @mock.patch.object(openphoto.main, "OpenPhoto") + def test_get(self, MockOpenPhoto): + get = MockOpenPhoto.return_value.get + get.return_value = "Result" + main(["-X", "GET", "-h", "test_host", "-e", "test_endpoint", "-F", + "field1=1", "-F", "field2=2"]) + MockOpenPhoto.assert_called_with(host="test_host") + get.assert_called_with("test_endpoint", field1="1", field2="2", + process_response=False) + # TODO: self.assertEq(mock_stdout.getvalue(), "Result") + + @mock.patch.object(openphoto.main, "OpenPhoto") + def test_post(self, MockOpenPhoto): + post = MockOpenPhoto.return_value.post + post.return_value = "Result" + main(["-X", "POST", "-h", "test_host", "-e", "test_endpoint", "-F", + "field1=1", "-F", "field2=2"]) + MockOpenPhoto.assert_called_with(host="test_host") + post.assert_called_with("test_endpoint", field1="1", field2="2", files={}, + process_response=False) + # TODO: self.assertEq(mock_stdout.getvalue(), "Result") + + @mock.patch.object(openphoto.main, "OpenPhoto") + def test_post_files(self, MockOpenPhoto): + post = MockOpenPhoto.return_value.post + main(["-X", "POST", "-F", "photo=@%s" % self.TEST_FILE]) + # It's not possible to directly compare the file object, so check it manually + files = post.call_args[1]["files"] + self.assertEqual(files.keys(), ["photo"]) + self.assertEqual(files["photo"].name, self.TEST_FILE) + + @mock.patch.object(sys, "exit", raise_exception) + def test_unknown_arg(self): + with self.assertRaises(TestException): + main(["hello"]) + # TODO: self.assertIn(mock_stdout.getvalue(), "Error: Unknown argument") + + @mock.patch.object(sys, "exit", raise_exception) + def test_unknown_option(self): + with self.assertRaises(TestException): + main(["--hello"]) + # TODO: self.assertIn(mock_stdout.getvalue(), "Error: no such option") + + @mock.patch.object(sys, "exit", raise_exception) + def test_unknown_config(self): + with self.assertRaises(TestException): + main(["--config=this_config_doesnt_exist"]) + # TODO: self.assertIn(mock_stdout.getvalue(), "No such file or directory") + # TODO: self.assertIn(mock_stdout.getvalue(), "You must create a configuration file") + # TODO: self.assertIn(mock_stdout.getvalue(), "To get your credentials") + + @mock.patch.object(openphoto.main, "OpenPhoto") + def test_verbose(self, _): + main(["-v"]) + # TODO: self.assertIn(mock_stdout.getvalue(), "Method: GET") + # TODO: self.assertIn(mock_stdout.getvalue(), "Endpoint: /photos/list.json") + + @mock.patch.object(openphoto.main, "OpenPhoto") + def test_pretty_print(self, MockOpenPhoto): + get = MockOpenPhoto.return_value.get + get.return_value = '{"test":1}' + main(["-p"]) + # TODO: self.assertEq(mock_stdout.getvalue(), '{\n "test":1\n}") From 0c1a4149a19072de5383d9402e0f7404cebd742d Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 10:49:15 +0100 Subject: [PATCH 06/58] Add album unit tests --- tests/unit/test_albums.py | 234 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 tests/unit/test_albums.py diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py new file mode 100644 index 0000000..4aad3e6 --- /dev/null +++ b/tests/unit/test_albums.py @@ -0,0 +1,234 @@ +from __future__ import unicode_literals +import mock +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + +import openphoto + +class TestAlbums(unittest.TestCase): + TEST_HOST = "test.example.com" + TEST_ALBUMS_DICT = [{"cover": {"id": "1a", "tags": ["tag1", "tag2"]}, + "id": "1", + "name": "Album 1", + "totalRows": 2}, + {"cover": {"id": "2b", "tags": ["tag3", "tag4"]}, + "id": "2", + "name": "Album 2", + "totalRows": 2}] + def setUp(self): + self.client = openphoto.OpenPhoto(host=self.TEST_HOST) + self.TEST_ALBUMS = [openphoto.objects.Album(self.client, album) + for album in self.TEST_ALBUMS_DICT] + + @staticmethod + def _return_value(result, message="", code=200): + return {"message": message, "code": code, "result": result} + +class TestAlbumsList(TestAlbums): + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_albums_list(self, mock): + mock.return_value = self._return_value(self.TEST_ALBUMS_DICT) + result = self.client.albums.list() + mock.assert_called_with("/albums/list.json") + self.assertEqual(len(result), 2) + self.assertEqual(result[0].id, "1") + self.assertEqual(result[0].name, "Album 1") + self.assertEqual(result[1].id, "2") + self.assertEqual(result[1].name, "Album 2") + + # TODO: cover should be updated to Photo object + @unittest.expectedFailure + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_albums_list_returns_cover_photos(self, mock): + mock.return_value = self._return_value(self.TEST_ALBUMS_DICT) + result = self.client.albums.list() + mock.assert_called_with("/albums/list.json") + self.assertEqual(len(result), 2) + self.assertEqual(result[0].id, "1") + self.assertEqual(result[0].name, "Album 1") + self.assertEqual(result[0].cover.id, "1a") + self.assertEqual(result[0].cover.tags, ["tag1", "tag2"]) + self.assertEqual(result[1].id, "2") + self.assertEqual(result[0].name, "Album 2") + self.assertEqual(result[1].cover.id, "2b") + self.assertEqual(result[1].cover.tags, ["tag3", "tag4"]) + +class TestAlbumCreate(TestAlbums): + # TODO: cover should be updated to Photo object + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_create(self, mock): + mock.return_value = self._return_value(self.TEST_ALBUMS_DICT[0]) + result = self.client.album.create(name="Test", foo="bar") + mock.assert_called_with("/album/create.json", name="Test", foo="bar") + self.assertEqual(result.id, "1") + self.assertEqual(result.name, "Album 1") + # self.assertEqual(result.cover.id, "1a") + # self.assertEqual(result.cover.tags, ["tag1", "tag2"]) + +class TestAlbumDelete(TestAlbums): + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_delete(self, mock): + mock.return_value = self._return_value(True) + result = self.client.album.delete(self.TEST_ALBUMS[0]) + mock.assert_called_with("/album/1/delete.json") + self.assertEqual(result, True) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_delete_id(self, mock): + mock.return_value = self._return_value(True) + result = self.client.album.delete("1") + mock.assert_called_with("/album/1/delete.json") + self.assertEqual(result, True) + + # TODO: album.delete should raise exception on failure + @unittest.expectedFailure + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_delete_failure_raises_exception(self, mock): + mock.return_value = self._return_value(False) + with self.assertRaises(openphoto.OpenPhotoError): + self.client.album.delete(self.TEST_ALBUMS[0]) + + # TODO: after deleting object fields, name and id should be set to None + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_object_delete(self, mock): + mock.return_value = self._return_value(True) + album = self.TEST_ALBUMS[0] + result = album.delete() + mock.assert_called_with("/album/1/delete.json") + self.assertEqual(result, True) + self.assertEqual(album.get_fields(), {}) + # self.assertEqual(album.id, None) + # self.assertEqual(album.name, None) + + # TODO: album.delete should raise exception on failure + @unittest.expectedFailure + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_object_delete_failure_raises_exception(self, mock): + mock.return_value = self._return_value(False) + with self.assertRaises(openphoto.OpenPhotoError): + self.TEST_ALBUMS[0].delete() + +class TestAlbumForm(TestAlbums): + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_form(self, mock): + with self.assertRaises(NotImplementedError): + self.client.album.form(self.TEST_ALBUMS[0]) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_form_id(self, mock): + with self.assertRaises(NotImplementedError): + self.client.album.form("1") + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_object_form(self, mock): + with self.assertRaises(NotImplementedError): + self.TEST_ALBUMS[0].form() + +class TestAlbumAddPhotos(TestAlbums): + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_add_photos(self, mock): + with self.assertRaises(NotImplementedError): + self.client.album.add_photos(self.TEST_ALBUMS[0], ["Photo Objects"]) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_add_photos_id(self, mock): + with self.assertRaises(NotImplementedError): + self.client.album.add_photos("1", ["Photo Objects"]) + + # TODO: object.add_photos should accept photos list as first parameter + @unittest.expectedFailure + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_object_add_photos(self, mock): + with self.assertRaises(NotImplementedError): + self.TEST_ALBUMS[0].add_photos(["Photo Objects"]) + +class TestAlbumRemovePhotos(TestAlbums): + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_remove_photos(self, mock): + with self.assertRaises(NotImplementedError): + self.client.album.remove_photos(self.TEST_ALBUMS[0], ["Photo Objects"]) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_remove_photos_id(self, mock): + with self.assertRaises(NotImplementedError): + self.client.album.remove_photos("1", ["Photo Objects"]) + + # TODO: object.remove_photos should accept photos list as first parameter + @unittest.expectedFailure + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_object_remove_photos(self, mock): + with self.assertRaises(NotImplementedError): + self.TEST_ALBUMS[0].remove_photos(["Photo Objects"]) + +class TestAlbumUpdate(TestAlbums): + # TODO: cover should be updated to Photo object + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_update(self, mock): + mock.return_value = self._return_value(self.TEST_ALBUMS_DICT[1]) + result = self.client.album.update(self.TEST_ALBUMS[0], name="Test") + mock.assert_called_with("/album/1/update.json", name="Test") + self.assertEqual(result.id, "2") + self.assertEqual(result.name, "Album 2") + # self.assertEqual(result.cover.id, "2b") + # self.assertEqual(result.cover.tags, ["tag3", "tag4"]) + + # TODO: cover should be updated to Photo object + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_update_id(self, mock): + mock.return_value = self._return_value(self.TEST_ALBUMS_DICT[1]) + result = self.client.album.update("1", name="Test") + mock.assert_called_with("/album/1/update.json", name="Test") + self.assertEqual(result.id, "2") + self.assertEqual(result.name, "Album 2") + # self.assertEqual(result.cover.id, "2b") + # self.assertEqual(result.cover.tags, ["tag3", "tag4"]) + + # TODO: cover should be updated to Photo object + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_album_object_update(self, mock): + mock.return_value = self._return_value(self.TEST_ALBUMS_DICT[1]) + album = self.TEST_ALBUMS[0] + album.update(name="Test") + mock.assert_called_with("/album/1/update.json", name="Test") + self.assertEqual(album.id, "2") + self.assertEqual(album.name, "Album 2") + # self.assertEqual(album.cover.id, "2b") + # self.assertEqual(album.cover.tags, ["tag3", "tag4"]) + +class TestAlbumView(TestAlbums): + # TODO: cover should be updated to Photo object + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_album_view(self, mock): + mock.return_value = self._return_value(self.TEST_ALBUMS_DICT[1]) + result = self.client.album.view(self.TEST_ALBUMS[0], name="Test") + mock.assert_called_with("/album/1/view.json", name="Test") + self.assertEqual(result.id, "2") + self.assertEqual(result.name, "Album 2") + # self.assertEqual(result.cover.id, "2b") + # self.assertEqual(result.cover.tags, ["tag3", "tag4"]) + + # TODO: cover should be updated to Photo object + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_album_view_id(self, mock): + mock.return_value = self._return_value(self.TEST_ALBUMS_DICT[1]) + result = self.client.album.view("1", name="Test") + mock.assert_called_with("/album/1/view.json", name="Test") + self.assertEqual(result.id, "2") + self.assertEqual(result.name, "Album 2") + # self.assertEqual(result.cover.id, "2b") + # self.assertEqual(result.cover.tags, ["tag3", "tag4"]) + + # TODO: cover should be updated to Photo object + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_album_object_view(self, mock): + mock.return_value = self._return_value(self.TEST_ALBUMS_DICT[1]) + album = self.TEST_ALBUMS[0] + album.view(name="Test") + mock.assert_called_with("/album/1/view.json", name="Test") + self.assertEqual(album.id, "2") + self.assertEqual(album.name, "Album 2") + # self.assertEqual(album.cover.id, "2b") + # self.assertEqual(album.cover.tags, ["tag3", "tag4"]) + From e3e09bd2246b0c35338c298bc87f234f180c0525 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 10:50:52 +0100 Subject: [PATCH 07/58] Check that local object's fields are empty after photo is deleted --- tests/unit/test_photos.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index a942028..8b05052 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -109,12 +109,16 @@ class TestPhotoDelete(TestPhotos): with self.assertRaises(openphoto.OpenPhotoError): self.client.photo.delete(self.TEST_PHOTOS[0]) + # TODO: after deleting object fields, name and id should be set to None @mock.patch.object(openphoto.OpenPhoto, 'post') def test_photo_object_delete(self, mock): mock.return_value = self._return_value(True) - result = self.TEST_PHOTOS[0].delete() + photo = self.TEST_PHOTOS[0] + result = photo.delete() mock.assert_called_with("/photo/1a/delete.json") self.assertEqual(result, True) + self.assertEqual(photo.get_fields(), {}) + # self.assertEqual(photo.id, None) # TODO: photo.delete should raise exception on failure @unittest.expectedFailure From 32544878d0ced00a202aec9eb5200ed3dfe0ca5d Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 10:53:08 +0100 Subject: [PATCH 08/58] Rename transform tests to be consistent --- tests/unit/test_photos.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index 8b05052..932cf1a 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -345,21 +345,21 @@ class TestPhotoNextPrevious(TestPhotos): class TestPhotoTransform(TestPhotos): @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_transform(self, mock): + def test_photo_transform(self, mock): mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) result = self.client.photo.transform(self.TEST_PHOTOS[0], rotate="90") mock.assert_called_with("/photo/1a/transform.json", rotate="90") self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[1]) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_transform_id(self, mock): + def test_photo_transform_id(self, mock): mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) result = self.client.photo.transform("1a", rotate="90") mock.assert_called_with("/photo/1a/transform.json", rotate="90") self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[1]) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_object_transform(self, mock): + def test_photo_object_transform(self, mock): mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) photo = self.TEST_PHOTOS[0] photo.transform(rotate="90") From b22a04f07132f62ac0003ca48e66b3e40c6d8cd8 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 10:56:18 +0100 Subject: [PATCH 09/58] TestHttpErrors shouldn't inherit from TestHttp --- tests/unit/test_http_errors.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_http_errors.py b/tests/unit/test_http_errors.py index 80eec6d..b25f78c 100644 --- a/tests/unit/test_http_errors.py +++ b/tests/unit/test_http_errors.py @@ -7,10 +7,23 @@ except ImportError: import unittest import openphoto -from tests.unit.test_http import TestHttp -class TestHttpErrors(TestHttp): - def _register_uri(self, method, uri=TestHttp.TEST_URI, +class TestHttpErrors(unittest.TestCase): + TEST_HOST = "test.example.com" + TEST_ENDPOINT = "test.json" + TEST_URI = "http://%s/%s" % (TEST_HOST, TEST_ENDPOINT) + TEST_DATA = {"message": "Test Message", + "code": 200, + "result": "Test Result"} + TEST_OAUTH = {"consumer_key": "dummy", + "consumer_secret": "dummy", + "token": "dummy", + "token_secret": "dummy"} + + def setUp(self): + self.client = openphoto.OpenPhoto(host=self.TEST_HOST, **self.TEST_OAUTH) + + def _register_uri(self, method, uri=TEST_URI, data=None, body=None, status=200, **kwds): """Convenience wrapper around httpretty.register_uri""" if data is None: From f083503b7a9c76ac56aa50a6390ece4539b2c265 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 11:19:02 +0100 Subject: [PATCH 10/58] Capture and check stdout/stderr during cli tests --- tests/unit/test_cli.py | 51 +++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 4733690..46f9f65 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -1,5 +1,6 @@ import os import sys +from StringIO import StringIO import mock try: import unittest2 as unittest # Python2.6 @@ -19,19 +20,22 @@ class TestCli(unittest.TestCase): TEST_FILE = os.path.join("tests", "unit", "data", "test_file.txt") @mock.patch.object(openphoto.main, "OpenPhoto") - def test_defaults(self, MockOpenPhoto): + @mock.patch('sys.stdout', new_callable=StringIO) + def test_defaults(self, _, MockOpenPhoto): get = MockOpenPhoto.return_value.get main([]) MockOpenPhoto.assert_called_with(config_file=None) get.assert_called_with("/photos/list.json", process_response=False) @mock.patch.object(openphoto.main, "OpenPhoto") - def test_config(self, MockOpenPhoto): + @mock.patch('sys.stdout', new_callable=StringIO) + def test_config(self, _, MockOpenPhoto): main(["--config=test"]) MockOpenPhoto.assert_called_with(config_file="test") @mock.patch.object(openphoto.main, "OpenPhoto") - def test_get(self, MockOpenPhoto): + @mock.patch('sys.stdout', new_callable=StringIO) + def test_get(self, mock_stdout, MockOpenPhoto): get = MockOpenPhoto.return_value.get get.return_value = "Result" main(["-X", "GET", "-h", "test_host", "-e", "test_endpoint", "-F", @@ -39,10 +43,11 @@ class TestCli(unittest.TestCase): MockOpenPhoto.assert_called_with(host="test_host") get.assert_called_with("test_endpoint", field1="1", field2="2", process_response=False) - # TODO: self.assertEq(mock_stdout.getvalue(), "Result") + self.assertEqual(mock_stdout.getvalue(), "Result\n") @mock.patch.object(openphoto.main, "OpenPhoto") - def test_post(self, MockOpenPhoto): + @mock.patch('sys.stdout', new_callable=StringIO) + def test_post(self, mock_stdout, MockOpenPhoto): post = MockOpenPhoto.return_value.post post.return_value = "Result" main(["-X", "POST", "-h", "test_host", "-e", "test_endpoint", "-F", @@ -50,10 +55,11 @@ class TestCli(unittest.TestCase): MockOpenPhoto.assert_called_with(host="test_host") post.assert_called_with("test_endpoint", field1="1", field2="2", files={}, process_response=False) - # TODO: self.assertEq(mock_stdout.getvalue(), "Result") + self.assertEqual(mock_stdout.getvalue(), "Result\n") @mock.patch.object(openphoto.main, "OpenPhoto") - def test_post_files(self, MockOpenPhoto): + @mock.patch('sys.stdout', new_callable=StringIO) + def test_post_files(self, _, MockOpenPhoto): post = MockOpenPhoto.return_value.post main(["-X", "POST", "-F", "photo=@%s" % self.TEST_FILE]) # It's not possible to directly compare the file object, so check it manually @@ -62,34 +68,39 @@ class TestCli(unittest.TestCase): self.assertEqual(files["photo"].name, self.TEST_FILE) @mock.patch.object(sys, "exit", raise_exception) - def test_unknown_arg(self): + @mock.patch('sys.stderr', new_callable=StringIO) + def test_unknown_arg(self, mock_stderr): with self.assertRaises(TestException): main(["hello"]) - # TODO: self.assertIn(mock_stdout.getvalue(), "Error: Unknown argument") + self.assertIn("error: Unknown argument", mock_stderr.getvalue()) @mock.patch.object(sys, "exit", raise_exception) - def test_unknown_option(self): + @mock.patch('sys.stderr', new_callable=StringIO) + def test_unknown_option(self, mock_stderr): with self.assertRaises(TestException): main(["--hello"]) - # TODO: self.assertIn(mock_stdout.getvalue(), "Error: no such option") + self.assertIn("error: no such option", mock_stderr.getvalue()) @mock.patch.object(sys, "exit", raise_exception) - def test_unknown_config(self): + @mock.patch('sys.stdout', new_callable=StringIO) + def test_unknown_config(self, mock_stdout): with self.assertRaises(TestException): main(["--config=this_config_doesnt_exist"]) - # TODO: self.assertIn(mock_stdout.getvalue(), "No such file or directory") - # TODO: self.assertIn(mock_stdout.getvalue(), "You must create a configuration file") - # TODO: self.assertIn(mock_stdout.getvalue(), "To get your credentials") + self.assertIn("No such file or directory", mock_stdout.getvalue()) + self.assertIn("You must create a configuration file", mock_stdout.getvalue()) + self.assertIn("To get your credentials", mock_stdout.getvalue()) @mock.patch.object(openphoto.main, "OpenPhoto") - def test_verbose(self, _): + @mock.patch('sys.stdout', new_callable=StringIO) + def test_verbose(self, mock_stdout, _): main(["-v"]) - # TODO: self.assertIn(mock_stdout.getvalue(), "Method: GET") - # TODO: self.assertIn(mock_stdout.getvalue(), "Endpoint: /photos/list.json") + self.assertIn("Method: GET", mock_stdout.getvalue()) + self.assertIn("Endpoint: /photos/list.json", mock_stdout.getvalue()) @mock.patch.object(openphoto.main, "OpenPhoto") - def test_pretty_print(self, MockOpenPhoto): + @mock.patch('sys.stdout', new_callable=StringIO) + def test_pretty_print(self, mock_stdout, MockOpenPhoto): get = MockOpenPhoto.return_value.get get.return_value = '{"test":1}' main(["-p"]) - # TODO: self.assertEq(mock_stdout.getvalue(), '{\n "test":1\n}") + self.assertEqual(mock_stdout.getvalue(), '{\n "test":1\n}\n') From 9319f903ac5c46ac1421a59fea7f48ca1262944d Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 12:16:58 +0100 Subject: [PATCH 11/58] Added tag unit tests --- tests/unit/test_tags.py | 114 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 tests/unit/test_tags.py diff --git a/tests/unit/test_tags.py b/tests/unit/test_tags.py new file mode 100644 index 0000000..562092c --- /dev/null +++ b/tests/unit/test_tags.py @@ -0,0 +1,114 @@ +from __future__ import unicode_literals +import mock +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + +import openphoto + +class TestTags(unittest.TestCase): + TEST_HOST = "test.example.com" + TEST_TAGS_DICT = [{"count": 11, "id":"tag1"}, + {"count": 5, "id":"tag2"}] + + def setUp(self): + self.client = openphoto.OpenPhoto(host=self.TEST_HOST) + self.TEST_TAGS = [openphoto.objects.Tag(self.client, tag) + for tag in self.TEST_TAGS_DICT] + + @staticmethod + def _return_value(result, message="", code=200): + return {"message": message, "code": code, "result": result} + +class TestTagsList(TestTags): + @mock.patch.object(openphoto.OpenPhoto, 'get') + def test_tags_list(self, mock): + mock.return_value = self._return_value(self.TEST_TAGS_DICT) + result = self.client.tags.list() + mock.assert_called_with("/tags/list.json") + self.assertEqual(len(result), 2) + self.assertEqual(result[0].id, "tag1") + self.assertEqual(result[0].count, 11) + self.assertEqual(result[1].id, "tag2") + self.assertEqual(result[1].count, 5) + +class TestTagCreate(TestTags): + # TODO: should return a tag object, not a result dict + @unittest.expectedFailure + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_tag_create(self, mock): + mock.return_value = self._return_value(self.TEST_TAGS_DICT[0]) + result = self.client.tag.create(tag="Test", foo="bar") + mock.assert_called_with("/tag/create.json", tag="Test", foo="bar") + self.assertEqual(result.id, "tag1") + self.assertEqual(result.count, 11) + +class TestTagDelete(TestTags): + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_tag_delete(self, mock): + mock.return_value = self._return_value(True) + result = self.client.tag.delete(self.TEST_TAGS[0]) + mock.assert_called_with("/tag/tag1/delete.json") + self.assertEqual(result, True) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_tag_delete_id(self, mock): + mock.return_value = self._return_value(True) + result = self.client.tag.delete("tag1") + mock.assert_called_with("/tag/tag1/delete.json") + self.assertEqual(result, True) + + # TODO: tag.delete should raise exception on failure + @unittest.expectedFailure + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_tag_delete_failure_raises_exception(self, mock): + mock.return_value = self._return_value(False) + with self.assertRaises(openphoto.OpenPhotoError): + self.client.tag.delete(self.TEST_TAGS[0]) + + # TODO: after deleting object fields, id should be set to None + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_tag_object_delete(self, mock): + mock.return_value = self._return_value(True) + tag = self.TEST_TAGS[0] + result = tag.delete() + mock.assert_called_with("/tag/tag1/delete.json") + self.assertEqual(result, True) + self.assertEqual(tag.get_fields(), {}) + # self.assertEqual(tag.id, None) + + # TODO: tag.delete should raise exception on failure + @unittest.expectedFailure + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_tag_object_delete_failure_raises_exception(self, mock): + mock.return_value = self._return_value(False) + with self.assertRaises(openphoto.OpenPhotoError): + self.TEST_TAGS[0].delete() + +class TestTagUpdate(TestTags): + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_tag_update(self, mock): + mock.return_value = self._return_value(self.TEST_TAGS_DICT[1]) + result = self.client.tag.update(self.TEST_TAGS[0], name="Test") + mock.assert_called_with("/tag/tag1/update.json", name="Test") + self.assertEqual(result.id, "tag2") + self.assertEqual(result.count, 5) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_tag_update_id(self, mock): + mock.return_value = self._return_value(self.TEST_TAGS_DICT[1]) + result = self.client.tag.update("tag1", name="Test") + mock.assert_called_with("/tag/tag1/update.json", name="Test") + self.assertEqual(result.id, "tag2") + self.assertEqual(result.count, 5) + + @mock.patch.object(openphoto.OpenPhoto, 'post') + def test_tag_object_update(self, mock): + mock.return_value = self._return_value(self.TEST_TAGS_DICT[1]) + tag = self.TEST_TAGS[0] + tag.update(name="Test") + mock.assert_called_with("/tag/tag1/update.json", name="Test") + self.assertEqual(tag.id, "tag2") + self.assertEqual(tag.count, 5) + From d3a40368172ed94746fe075955e6a738b79dd5d9 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 12:43:41 +0100 Subject: [PATCH 12/58] Reorganise tests into unit and functional categories. Tox (and hence Travis) only runs unit tests Functional tests can be run manually, with the default Python version --- run_tests => run_functional_tests | 11 +++++------ tests/{ => data}/test_photo1.jpg | Bin tests/{ => data}/test_photo2.jpg | Bin tests/{ => data}/test_photo3.jpg | Bin tests/{api_versions => functional}/__init__.py | 0 tests/functional/api_versions/__init__.py | 0 tests/{ => functional}/api_versions/test_v1.py | 2 +- tests/{ => functional}/api_versions/test_v2.py | 2 +- tests/{ => functional}/test_albums.py | 4 ++-- tests/{ => functional}/test_base.py | 6 +++--- tests/{ => functional}/test_framework.py | 6 +++--- tests/{ => functional}/test_photos.py | 12 ++++++------ tests/{ => functional}/test_tags.py | 6 +++--- tests/{ => unit}/test_config.py | 0 tox.ini | 9 +++++++-- 15 files changed, 31 insertions(+), 27 deletions(-) rename run_tests => run_functional_tests (65%) rename tests/{ => data}/test_photo1.jpg (100%) rename tests/{ => data}/test_photo2.jpg (100%) rename tests/{ => data}/test_photo3.jpg (100%) rename tests/{api_versions => functional}/__init__.py (100%) create mode 100644 tests/functional/api_versions/__init__.py rename tests/{ => functional}/api_versions/test_v1.py (74%) rename tests/{ => functional}/api_versions/test_v2.py (88%) rename tests/{ => functional}/test_albums.py (97%) rename tests/{ => functional}/test_base.py (96%) rename tests/{ => functional}/test_framework.py (93%) rename tests/{ => functional}/test_photos.py (94%) rename tests/{ => functional}/test_tags.py (96%) rename tests/{ => unit}/test_config.py (100%) diff --git a/run_tests b/run_functional_tests similarity index 65% rename from run_tests rename to run_functional_tests index b800038..5208de8 100755 --- a/run_tests +++ b/run_functional_tests @@ -1,10 +1,9 @@ #!/bin/bash # -# Simple script to run all tests with multiple test servers -# across all supported Python versions +# Simple script to run all functional tests with multiple test servers # -# Default test server running latest self-hosted site +# Test server running latest self-hosted site tput setaf 3 echo @@ -12,7 +11,7 @@ echo "Testing latest self-hosted site..." tput sgr0 export OPENPHOTO_TEST_CONFIG=test unset OPENPHOTO_TEST_SERVER_API -tox $@ +python -m unittest discover --catch tests/functional # Test server running APIv1 OpenPhoto instance tput setaf 3 @@ -21,7 +20,7 @@ echo "Testing APIv1 self-hosted site..." tput sgr0 export OPENPHOTO_TEST_CONFIG=test-apiv1 export OPENPHOTO_TEST_SERVER_API=1 -tox $@ +python -m unittest discover --catch tests/functional # Test account on hosted trovebox.com site tput setaf 3 @@ -30,5 +29,5 @@ echo "Testing latest hosted site..." tput sgr0 export OPENPHOTO_TEST_CONFIG=test-hosted unset OPENPHOTO_TEST_SERVER_API -tox $@ +python -m unittest discover --catch tests/functional diff --git a/tests/test_photo1.jpg b/tests/data/test_photo1.jpg similarity index 100% rename from tests/test_photo1.jpg rename to tests/data/test_photo1.jpg diff --git a/tests/test_photo2.jpg b/tests/data/test_photo2.jpg similarity index 100% rename from tests/test_photo2.jpg rename to tests/data/test_photo2.jpg diff --git a/tests/test_photo3.jpg b/tests/data/test_photo3.jpg similarity index 100% rename from tests/test_photo3.jpg rename to tests/data/test_photo3.jpg diff --git a/tests/api_versions/__init__.py b/tests/functional/__init__.py similarity index 100% rename from tests/api_versions/__init__.py rename to tests/functional/__init__.py diff --git a/tests/functional/api_versions/__init__.py b/tests/functional/api_versions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/api_versions/test_v1.py b/tests/functional/api_versions/test_v1.py similarity index 74% rename from tests/api_versions/test_v1.py rename to tests/functional/api_versions/test_v1.py index 92baabb..aa3c652 100644 --- a/tests/api_versions/test_v1.py +++ b/tests/functional/api_versions/test_v1.py @@ -1,4 +1,4 @@ -from tests import test_albums, test_photos, test_tags +from tests.functional import test_albums, test_photos, test_tags class TestAlbumsV1(test_albums.TestAlbums): api_version = 1 diff --git a/tests/api_versions/test_v2.py b/tests/functional/api_versions/test_v2.py similarity index 88% rename from tests/api_versions/test_v2.py rename to tests/functional/api_versions/test_v2.py index 545e647..a2c425c 100644 --- a/tests/api_versions/test_v2.py +++ b/tests/functional/api_versions/test_v2.py @@ -2,7 +2,7 @@ try: import unittest2 as unittest except ImportError: import unittest -from tests import test_base, test_albums, test_photos, test_tags +from tests.functional import test_base, test_albums, test_photos, test_tags @unittest.skipIf(test_base.get_test_server_api() < 2, "Don't test future API versions") diff --git a/tests/test_albums.py b/tests/functional/test_albums.py similarity index 97% rename from tests/test_albums.py rename to tests/functional/test_albums.py index c48420e..95b28ab 100644 --- a/tests/test_albums.py +++ b/tests/functional/test_albums.py @@ -1,6 +1,6 @@ -import tests.test_base +from tests.functional import test_base -class TestAlbums(tests.test_base.TestBase): +class TestAlbums(test_base.TestBase): testcase_name = "album API" def test_create_delete(self): diff --git a/tests/test_base.py b/tests/functional/test_base.py similarity index 96% rename from tests/test_base.py rename to tests/functional/test_base.py index eb05caf..2c3926e 100644 --- a/tests/test_base.py +++ b/tests/functional/test_base.py @@ -128,13 +128,13 @@ class TestBase(unittest.TestCase): """ Upload three test photos """ album = cls.client.album.create(cls.TEST_ALBUM) photos = [ - cls.client.photo.upload("tests/test_photo1.jpg", + cls.client.photo.upload("tests/data/test_photo1.jpg", title=cls.TEST_TITLE, albums=album.id), - cls.client.photo.upload("tests/test_photo2.jpg", + cls.client.photo.upload("tests/data/test_photo2.jpg", title=cls.TEST_TITLE, albums=album.id), - cls.client.photo.upload("tests/test_photo3.jpg", + cls.client.photo.upload("tests/data/test_photo3.jpg", title=cls.TEST_TITLE, albums=album.id), ] diff --git a/tests/test_framework.py b/tests/functional/test_framework.py similarity index 93% rename from tests/test_framework.py rename to tests/functional/test_framework.py index 6d34f73..788ac13 100644 --- a/tests/test_framework.py +++ b/tests/functional/test_framework.py @@ -1,9 +1,9 @@ import logging import openphoto -import tests.test_base +from tests.functional import test_base -class TestFramework(tests.test_base.TestBase): +class TestFramework(test_base.TestBase): testcase_name = "framework" def setUp(self): @@ -27,7 +27,7 @@ class TestFramework(tests.test_base.TestBase): """ For all API versions >0, we get a generic hello world message """ - for api_version in range(1, tests.test_base.get_test_server_api() + 1): + for api_version in range(1, test_base.get_test_server_api() + 1): client = openphoto.OpenPhoto(config_file=self.config_file, api_version=api_version) result = client.get("hello.json") diff --git a/tests/test_photos.py b/tests/functional/test_photos.py similarity index 94% rename from tests/test_photos.py rename to tests/functional/test_photos.py index d0029a4..c6c9b57 100644 --- a/tests/test_photos.py +++ b/tests/functional/test_photos.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals import openphoto -import tests.test_base +from tests.functional import test_base -class TestPhotos(tests.test_base.TestBase): +class TestPhotos(test_base.TestBase): testcase_name = "photo API" def test_delete_upload(self): @@ -19,11 +19,11 @@ class TestPhotos(tests.test_base.TestBase): self.assertEqual(self.client.photos.list(), []) # Re-upload the photos, one of them using Bas64 encoding - ret_val = self.client.photo.upload("tests/test_photo1.jpg", + ret_val = self.client.photo.upload("tests/data/test_photo1.jpg", title=self.TEST_TITLE) - self.client.photo.upload("tests/test_photo2.jpg", + self.client.photo.upload("tests/data/test_photo2.jpg", title=self.TEST_TITLE) - self.client.photo.upload_encoded("tests/test_photo3.jpg", + self.client.photo.upload_encoded("tests/data/test_photo3.jpg", title=self.TEST_TITLE) # Check there are now three photos with the correct titles @@ -61,7 +61,7 @@ class TestPhotos(tests.test_base.TestBase): """ Ensure that duplicate photos are rejected """ # Attempt to upload a duplicate with self.assertRaises(openphoto.OpenPhotoDuplicateError): - self.client.photo.upload("tests/test_photo1.jpg", + self.client.photo.upload("tests/data/test_photo1.jpg", title=self.TEST_TITLE) # Check there are still three photos diff --git a/tests/test_tags.py b/tests/functional/test_tags.py similarity index 96% rename from tests/test_tags.py rename to tests/functional/test_tags.py index 40a577e..14ed6c0 100644 --- a/tests/test_tags.py +++ b/tests/functional/test_tags.py @@ -3,11 +3,11 @@ try: except ImportError: import unittest -import tests.test_base +from tests.functional import test_base -@unittest.skipIf(tests.test_base.get_test_server_api() == 1, +@unittest.skipIf(test_base.get_test_server_api() == 1, "The tag API didn't work at v1 - see frontend issue #927") -class TestTags(tests.test_base.TestBase): +class TestTags(test_base.TestBase): testcase_name = "tag API" def test_create_delete(self, tag_id="create_tag"): diff --git a/tests/test_config.py b/tests/unit/test_config.py similarity index 100% rename from tests/test_config.py rename to tests/unit/test_config.py diff --git a/tox.ini b/tox.ini index 7f1f87f..5e5c469 100644 --- a/tox.ini +++ b/tox.ini @@ -2,10 +2,15 @@ envlist = py26, py27, py33 [testenv] -commands = python -m unittest discover --catch +commands = python -m unittest discover --catch tests/unit +deps = + mock + httpretty [testenv:py26] -commands = unit2 discover --catch +commands = unit2 discover --catch tests/unit deps = + mock + httpretty unittest2 discover From 2147b41383c1dc1ff12fb8585498e302e5ea410f Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 13:17:33 +0100 Subject: [PATCH 13/58] Updated test documentation --- tests/README.markdown | 112 ++++++------------------------- tests/functional/README.markdown | 104 ++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 91 deletions(-) create mode 100644 tests/functional/README.markdown diff --git a/tests/README.markdown b/tests/README.markdown index 84eb6ab..8eb88a0 100644 --- a/tests/README.markdown +++ b/tests/README.markdown @@ -1,101 +1,31 @@ -Tests for the Open Photo API / Python Library +Tests for the OpenPhoto/Trovebox Python Library ======================= -#### OpenPhoto, a photo service for the masses + +The openphoto-python tests are split into two categories: + +###Unit Tests + +The unit tests mock out all HTTP requests, and verify that the various +components of the library are operating correctly. + +They run very quickly, and don't require any external test hosts. ---------------------------------------- -### Requirements -A computer, Python and an empty OpenPhoto test host. +#### Requirements + * mock >= 1.0.0 + * httpretty >= 0.6.1 ---------------------------------------- - -### Setting up +#### Running the tests +To run the unit tests: -Create a ``~/.config/openphoto/test`` config file containing the following: + python -m unittest discover tests/unit - # ~/.config/openphoto/test - host = your.host.com - consumerKey = your_consumer_key - consumerSecret = your_consumer_secret - token = your_access_token - tokenSecret = your_access_token_secret +###Functional Tests -Make sure this is an empty test server, **not a production OpenPhoto server!!!** +The functional tests check that the openphoto-python library interoperates +correctly with a real OpenPhoto/Trovebox server. -You can specify an alternate test config file with the following environment variable: +They are slow to run, and rely on a stable HTTP connection to a test server. - export OPENPHOTO_TEST_CONFIG=test2 - ---------------------------------------- - -### Running the tests - -The following instructions are for Python 2.7. You can adapt them for earlier -Python versions using the ``unittest2`` package. - - cd /path/to/openphoto-python - python -m unittest discover -c - -The "-c" lets you stop the tests gracefully with \[CTRL\]-c. - -The easiest way to run a subset of the tests is with the ``nose`` package: - - cd /path/to/openphoto-python - nosetests -v -s --nologcapture tests/test_albums.py:TestAlbums.test_view - -All HTTP requests and responses are recorded in the file ``tests.log``. - -You can enable more verbose output to stdout with the following environment variable: - - export OPENPHOTO_TEST_DEBUG=1 - ---------------------------------------- - -### Test Details - -These tests are intended to verify the openphoto-python library. -They don't provide comprehensive testing of the OpenPhoto API, -there are PHP unit tests for that. - -Each test class is run as follows: - -**SetUpClass:** - -Check that the server is empty - -**SetUp:** - -Ensure there are: - - * Three test photos - * A single test tag applied to each - * A single album containing all three photos - -**TearDownClass:** - -Remove all photos, tags and albums - -### Testing old servers - -By default, all currently supported API versions will be tested. -It's useful to test servers that only support older API versions. -To restrict the testing to a specific maximum API version, use the -``OPENPHOTO_TEST_SERVER_API`` environment variable. - -For example, to restrict testing to APIv1 and APIv2: - - export OPENPHOTO_TEST_SERVER_API=2 - - -### Full Regression Test - -The ``run_tests`` script uses the ``tox`` package to run a full regression across: - * Multiple Python versions - * All supported API versions - -To use it, you must set up multiple OpenPhoto instances and create the following -config files containing your credentials: - - test : Latest self-hosted site - test-apiv1 : APIv1 self-hosted site - test-hosted : Credentials for test account on trovebox.com +For full details, see the [functional test README file](functional/README.markdown). diff --git a/tests/functional/README.markdown b/tests/functional/README.markdown new file mode 100644 index 0000000..7b0e6b4 --- /dev/null +++ b/tests/functional/README.markdown @@ -0,0 +1,104 @@ +Functional Tests for the OpenPhoto/Trovebox Python Library +======================= + +These functional tests check that the openphoto-python library interoperates +correctly with a real OpenPhoto/Trovebox server. + +They are slow to run, and require a stable HTTP connection to a test server. + +---------------------------------------- + +### Requirements +A computer, Python and an empty OpenPhoto/Trovebox test host. + +--------------------------------------- + +### Setting up + +Create a ``~/.config/openphoto/test`` config file containing the following: + + # ~/.config/openphoto/test + host = your.host.com + consumerKey = your_consumer_key + consumerSecret = your_consumer_secret + token = your_access_token + tokenSecret = your_access_token_secret + +Make sure this is an empty test server, **not a production OpenPhoto server!!!** + +You can specify an alternate test config file with the following environment variable: + + export OPENPHOTO_TEST_CONFIG=test2 + +--------------------------------------- + +### Running the tests + +The following instructions are for Python 2.7. You can adapt them for earlier +Python versions using the ``unittest2`` package. + + cd /path/to/openphoto-python + python -m unittest discover -c + +The "-c" lets you stop the tests gracefully with \[CTRL\]-c. + +The easiest way to run a subset of the tests is with the ``nose`` package: + + cd /path/to/openphoto-python + nosetests -v -s --nologcapture tests/test_albums.py:TestAlbums.test_view + +All HTTP requests and responses are recorded in the file ``tests.log``. + +You can enable more verbose output to stdout with the following environment variable: + + export OPENPHOTO_TEST_DEBUG=1 + +--------------------------------------- + +### Test Details + +These tests are intended to verify the openphoto-python library. +They don't provide comprehensive testing of the OpenPhoto API, +there are PHP unit tests for that. + +Each test class is run as follows: + +**SetUpClass:** + +Check that the server is empty + +**SetUp:** + +Ensure there are: + + * Three test photos + * A single test tag applied to each + * A single album containing all three photos + +**TearDownClass:** + +Remove all photos, tags and albums + +### Testing old servers + +By default, all currently supported API versions will be tested. +It's useful to test servers that only support older API versions. +To restrict the testing to a specific maximum API version, use the +``OPENPHOTO_TEST_SERVER_API`` environment variable. + +For example, to restrict testing to APIv1 and APIv2: + + export OPENPHOTO_TEST_SERVER_API=2 + + +### Full Regression Test + +The ``run_functional_tests`` script runs a full regression across +all supported API versions. + +To use it, you must set up multiple OpenPhoto instances and create the following +config files containing your credentials: + + test : Latest self-hosted site + test-apiv1 : APIv1 self-hosted site + test-hosted : Credentials for test account on trovebox.com From 134409b8148ed94228a2b1462cc54b1e765f59b9 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 15:36:44 +0100 Subject: [PATCH 14/58] Update test readme file --- tests/README.markdown | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/README.markdown b/tests/README.markdown index 8eb88a0..320b5b3 100644 --- a/tests/README.markdown +++ b/tests/README.markdown @@ -1,31 +1,29 @@ -Tests for the OpenPhoto/Trovebox Python Library +OpenPhoto/Trovebox Python Testing ======================= -The openphoto-python tests are split into two categories: - ###Unit Tests The unit tests mock out all HTTP requests, and verify that the various components of the library are operating correctly. -They run very quickly, and don't require any external test hosts. +They run very quickly and don't require any external test hosts. ----------------------------------------- #### Requirements * mock >= 1.0.0 * httpretty >= 0.6.1 -#### Running the tests -To run the unit tests: +#### Running the Unit Tests python -m unittest discover tests/unit +---------------------------------------- + ###Functional Tests The functional tests check that the openphoto-python library interoperates correctly with a real OpenPhoto/Trovebox server. -They are slow to run, and rely on a stable HTTP connection to a test server. +They are slow to run and rely on a stable HTTP connection to a test server. For full details, see the [functional test README file](functional/README.markdown). From a4f7a1194dc05a3cef4ff94be9c7ff8f7dc25f6a Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 15:39:52 +0100 Subject: [PATCH 15/58] Update functional test README file --- tests/functional/README.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/README.markdown b/tests/functional/README.markdown index 7b0e6b4..64601a8 100644 --- a/tests/functional/README.markdown +++ b/tests/functional/README.markdown @@ -1,4 +1,4 @@ -Functional Tests for the OpenPhoto/Trovebox Python Library +Functional Testing ======================= These functional tests check that the openphoto-python library interoperates @@ -93,7 +93,7 @@ For example, to restrict testing to APIv1 and APIv2: ### Full Regression Test -The ``run_functional_tests`` script runs a full regression across +The ``run_functional_tests`` script runs all functional tests against all supported API versions. To use it, you must set up multiple OpenPhoto instances and create the following From 5825a751c3a4a75c30150a5c20447bfe24037a88 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 17:02:12 +0100 Subject: [PATCH 16/58] Removed config secrets from travis setup, since we're now only running the unit tests --- .travis.yml | 10 +--------- .travis/run_travis | 19 ------------------- 2 files changed, 1 insertion(+), 28 deletions(-) delete mode 100755 .travis/run_travis diff --git a/.travis.yml b/.travis.yml index b03f4e1..ee74c8c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,15 +4,7 @@ install: - pip install tox --use-mirrors - .travis/install_pylint -env: - global: - - secure: "CQNSUBhVyedtCbioKkoueZsBFw0H+EnrPPNQIO+v3ieniPhFQbCoaCOA6rZG\n1MH1oIz5GSB1hv48jLCSSDppYOX1nKlLUFAepm9h7HSv2MaBzENKcp3ipBLP\nn8QEhVCkeWVnTkRB+IWrQHiW+8vHZ1iaERjlX9cMav7rBzzvK9g=" - - secure: "e5xYBGGzn6x06hmofDJ+tuS8iAVPuFNGqizR8cA6+2W4rSQEbh7NcKKeAvB5\n8qlmBonupo0wttkewh2hpnxvaXV7uS4C0Qt/h087Bu4cPkJMENWq++CrDo6G\nwjkAu6x6YDkzuMuxa5BTWU9hAQVX1jq+cjYOmORhw/v5FFukN44=" - - secure: "aU95NQmiY2ytyGRywEQvblN1YinIHpe/L9jnYlxazhfdHr+WXZd5aXC4Ze/U\nqlsHR+PGjycPHUCykJ/W5KU68tAX9r3PQgaQlfWd1cT89paY4givtoHiTz+f\nGu2I3BexskJ58NcUEDp6MEJqEuIXiQYUpoQ+6rNzvpe427xt6R0=" - - secure: "ilNFM41mePkXMpvK/6T7s3vsQCN36XoiHnR7Fxrnpur9sXOfwB8A1Kw7CpbM\n5rxc2QNj7SPrT2K49QE8fUKHIl88a2MqCf+ujy9mG7WgKdxYazIxrhyHCNKO\nZ47r38kijW92GnSX4KTDeORfouZgR21BpDTfoCvspiWzWzG/fYE=" - - secure: "YdUPDO7sTUTG2EwUlrxwOWKhlGXJiIK+RBWDspqvM8UQV4CQjzIsRX8urUIN\nSpSjJOfbIw25S+AsLpEBye8OJMncm/16Xp7PL5tlkNmRC12mPVG8f+wpOkrW\nt8v+2Cv/prYDn0tjoqnV1f5Nv5cEW6kAkG19UQ4QBgQzirtrs9Y=" - -script: .travis/run_travis +script: tox -e py27 after_script: # Run Pylint diff --git a/.travis/run_travis b/.travis/run_travis deleted file mode 100755 index bfaee74..0000000 --- a/.travis/run_travis +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Create a config file containing the test host's secrets - -CONFIG_DIR=~/.config/openphoto -CONFIG_FILE=$CONFIG_DIR/test - -mkdir ~/.config -mkdir $CONFIG_DIR - -echo "host = $OP_HOST" >> $CONFIG_FILE -echo "consumerKey = $OP_CONSUMER_KEY" >> $CONFIG_FILE -echo "consumerSecret = $OP_CONSUMER_SECRET" >> $CONFIG_FILE -echo "token = $OP_TOKEN" >> $CONFIG_FILE -echo "tokenSecret = $OP_TOKEN_SECRET" >> $CONFIG_FILE - -# Run the tests - -tox -e py27 From 77fef49c6433a3e6041ffed63d584378a8e3a4c1 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 18:57:12 +0100 Subject: [PATCH 17/58] PyLint fixes --- tests/unit/test_albums.py | 164 +++++++------ tests/unit/test_cli.py | 56 +++-- tests/unit/test_config.py | 3 +- tests/unit/test_http.py | 114 +++++---- tests/unit/test_http_errors.py | 129 +++++++--- tests/unit/test_photos.py | 423 +++++++++++++++++++-------------- tests/unit/test_tags.py | 92 ++++--- 7 files changed, 593 insertions(+), 388 deletions(-) diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py index 4aad3e6..a117bd5 100644 --- a/tests/unit/test_albums.py +++ b/tests/unit/test_albums.py @@ -8,8 +8,8 @@ except ImportError: import openphoto class TestAlbums(unittest.TestCase): - TEST_HOST = "test.example.com" - TEST_ALBUMS_DICT = [{"cover": {"id": "1a", "tags": ["tag1", "tag2"]}, + test_host = "test.example.com" + test_albums_dict = [{"cover": {"id": "1a", "tags": ["tag1", "tag2"]}, "id": "1", "name": "Album 1", "totalRows": 2}, @@ -18,9 +18,9 @@ class TestAlbums(unittest.TestCase): "name": "Album 2", "totalRows": 2}] def setUp(self): - self.client = openphoto.OpenPhoto(host=self.TEST_HOST) - self.TEST_ALBUMS = [openphoto.objects.Album(self.client, album) - for album in self.TEST_ALBUMS_DICT] + self.client = openphoto.OpenPhoto(host=self.test_host) + self.test_albums = [openphoto.objects.Album(self.client, album) + for album in self.test_albums_dict] @staticmethod def _return_value(result, message="", code=200): @@ -28,10 +28,11 @@ class TestAlbums(unittest.TestCase): class TestAlbumsList(TestAlbums): @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_albums_list(self, mock): - mock.return_value = self._return_value(self.TEST_ALBUMS_DICT) + def test_albums_list(self, mock_get): + """Check that the album list is returned correctly""" + mock_get.return_value = self._return_value(self.test_albums_dict) result = self.client.albums.list() - mock.assert_called_with("/albums/list.json") + mock_get.assert_called_with("/albums/list.json") self.assertEqual(len(result), 2) self.assertEqual(result[0].id, "1") self.assertEqual(result[0].name, "Album 1") @@ -41,10 +42,11 @@ class TestAlbumsList(TestAlbums): # TODO: cover should be updated to Photo object @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_albums_list_returns_cover_photos(self, mock): - mock.return_value = self._return_value(self.TEST_ALBUMS_DICT) + def test_albums_list_returns_cover_photos(self, mock_get): + """Check that the album list returns cover photo objects""" + mock_get.return_value = self._return_value(self.test_albums_dict) result = self.client.albums.list() - mock.assert_called_with("/albums/list.json") + mock_get.assert_called_with("/albums/list.json") self.assertEqual(len(result), 2) self.assertEqual(result[0].id, "1") self.assertEqual(result[0].name, "Album 1") @@ -58,10 +60,12 @@ class TestAlbumsList(TestAlbums): class TestAlbumCreate(TestAlbums): # TODO: cover should be updated to Photo object @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_create(self, mock): - mock.return_value = self._return_value(self.TEST_ALBUMS_DICT[0]) + def test_album_create(self, mock_post): + """Check that an album can be created""" + mock_post.return_value = self._return_value(self.test_albums_dict[0]) result = self.client.album.create(name="Test", foo="bar") - mock.assert_called_with("/album/create.json", name="Test", foo="bar") + mock_post.assert_called_with("/album/create.json", name="Test", + foo="bar") self.assertEqual(result.id, "1") self.assertEqual(result.name, "Album 1") # self.assertEqual(result.cover.id, "1a") @@ -69,34 +73,38 @@ class TestAlbumCreate(TestAlbums): class TestAlbumDelete(TestAlbums): @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_delete(self, mock): - mock.return_value = self._return_value(True) - result = self.client.album.delete(self.TEST_ALBUMS[0]) - mock.assert_called_with("/album/1/delete.json") + def test_album_delete(self, mock_post): + """Check that an album can be deleted""" + mock_post.return_value = self._return_value(True) + result = self.client.album.delete(self.test_albums[0]) + mock_post.assert_called_with("/album/1/delete.json") self.assertEqual(result, True) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_delete_id(self, mock): - mock.return_value = self._return_value(True) + def test_album_delete_id(self, mock_post): + """Check that an album can be deleted using its ID""" + mock_post.return_value = self._return_value(True) result = self.client.album.delete("1") - mock.assert_called_with("/album/1/delete.json") + mock_post.assert_called_with("/album/1/delete.json") self.assertEqual(result, True) # TODO: album.delete should raise exception on failure @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_delete_failure_raises_exception(self, mock): - mock.return_value = self._return_value(False) + def test_album_delete_failure(self, mock_post): + """Check that an exception is raised if an album cannot be deleted""" + mock_post.return_value = self._return_value(False) with self.assertRaises(openphoto.OpenPhotoError): - self.client.album.delete(self.TEST_ALBUMS[0]) + self.client.album.delete(self.test_albums[0]) # TODO: after deleting object fields, name and id should be set to None @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_object_delete(self, mock): - mock.return_value = self._return_value(True) - album = self.TEST_ALBUMS[0] + def test_album_object_delete(self, mock_post): + """Check that an album can be deleted using the album object directly""" + mock_post.return_value = self._return_value(True) + album = self.test_albums[0] result = album.delete() - mock.assert_called_with("/album/1/delete.json") + mock_post.assert_called_with("/album/1/delete.json") self.assertEqual(result, True) self.assertEqual(album.get_fields(), {}) # self.assertEqual(album.id, None) @@ -105,70 +113,85 @@ class TestAlbumDelete(TestAlbums): # TODO: album.delete should raise exception on failure @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_object_delete_failure_raises_exception(self, mock): - mock.return_value = self._return_value(False) + def test_album_object_delete_failure(self, mock_post): + """ + Check that an exception is raised if an album cannot be deleted + when using the album object directly + """ + mock_post.return_value = self._return_value(False) with self.assertRaises(openphoto.OpenPhotoError): - self.TEST_ALBUMS[0].delete() + self.test_albums[0].delete() class TestAlbumForm(TestAlbums): @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_form(self, mock): + def test_album_form(self, _): + """ If album.form gets implemented, write a test! """ with self.assertRaises(NotImplementedError): - self.client.album.form(self.TEST_ALBUMS[0]) + self.client.album.form(self.test_albums[0]) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_form_id(self, mock): + def test_album_form_id(self, _): + """ If album.form gets implemented, write a test! """ with self.assertRaises(NotImplementedError): self.client.album.form("1") @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_object_form(self, mock): + def test_album_object_form(self, _): + """ If album.form gets implemented, write a test! """ with self.assertRaises(NotImplementedError): - self.TEST_ALBUMS[0].form() + self.test_albums[0].form() class TestAlbumAddPhotos(TestAlbums): @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_add_photos(self, mock): + def test_album_add_photos(self, _): + """ If album.add_photos gets implemented, write a test! """ with self.assertRaises(NotImplementedError): - self.client.album.add_photos(self.TEST_ALBUMS[0], ["Photo Objects"]) + self.client.album.add_photos(self.test_albums[0], ["Photo Objects"]) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_add_photos_id(self, mock): + def test_album_add_photos_id(self, _): + """ If album.add_photos gets implemented, write a test! """ with self.assertRaises(NotImplementedError): self.client.album.add_photos("1", ["Photo Objects"]) # TODO: object.add_photos should accept photos list as first parameter @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_object_add_photos(self, mock): + def test_album_object_add_photos(self, _): + """ If album.add_photos gets implemented, write a test! """ with self.assertRaises(NotImplementedError): - self.TEST_ALBUMS[0].add_photos(["Photo Objects"]) + self.test_albums[0].add_photos(["Photo Objects"]) class TestAlbumRemovePhotos(TestAlbums): @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_remove_photos(self, mock): + def test_album_remove_photos(self, _): + """ If album.remove_photos gets implemented, write a test! """ with self.assertRaises(NotImplementedError): - self.client.album.remove_photos(self.TEST_ALBUMS[0], ["Photo Objects"]) + self.client.album.remove_photos(self.test_albums[0], + ["Photo Objects"]) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_remove_photos_id(self, mock): + def test_album_remove_photos_id(self, _): + """ If album.remove_photos gets implemented, write a test! """ with self.assertRaises(NotImplementedError): self.client.album.remove_photos("1", ["Photo Objects"]) # TODO: object.remove_photos should accept photos list as first parameter @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_object_remove_photos(self, mock): + def test_album_object_remove_photos(self, _): + """ If album.remove_photos gets implemented, write a test! """ with self.assertRaises(NotImplementedError): - self.TEST_ALBUMS[0].remove_photos(["Photo Objects"]) + self.test_albums[0].remove_photos(["Photo Objects"]) class TestAlbumUpdate(TestAlbums): # TODO: cover should be updated to Photo object @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_update(self, mock): - mock.return_value = self._return_value(self.TEST_ALBUMS_DICT[1]) - result = self.client.album.update(self.TEST_ALBUMS[0], name="Test") - mock.assert_called_with("/album/1/update.json", name="Test") + def test_album_update(self, mock_post): + """Check that an album can be updated""" + mock_post.return_value = self._return_value(self.test_albums_dict[1]) + result = self.client.album.update(self.test_albums[0], name="Test") + mock_post.assert_called_with("/album/1/update.json", name="Test") self.assertEqual(result.id, "2") self.assertEqual(result.name, "Album 2") # self.assertEqual(result.cover.id, "2b") @@ -176,10 +199,11 @@ class TestAlbumUpdate(TestAlbums): # TODO: cover should be updated to Photo object @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_update_id(self, mock): - mock.return_value = self._return_value(self.TEST_ALBUMS_DICT[1]) + def test_album_update_id(self, mock_post): + """Check that an album can be updated using its ID""" + mock_post.return_value = self._return_value(self.test_albums_dict[1]) result = self.client.album.update("1", name="Test") - mock.assert_called_with("/album/1/update.json", name="Test") + mock_post.assert_called_with("/album/1/update.json", name="Test") self.assertEqual(result.id, "2") self.assertEqual(result.name, "Album 2") # self.assertEqual(result.cover.id, "2b") @@ -187,11 +211,12 @@ class TestAlbumUpdate(TestAlbums): # TODO: cover should be updated to Photo object @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_album_object_update(self, mock): - mock.return_value = self._return_value(self.TEST_ALBUMS_DICT[1]) - album = self.TEST_ALBUMS[0] + def test_album_object_update(self, mock_post): + """Check that an album can be updated using the album object directly""" + mock_post.return_value = self._return_value(self.test_albums_dict[1]) + album = self.test_albums[0] album.update(name="Test") - mock.assert_called_with("/album/1/update.json", name="Test") + mock_post.assert_called_with("/album/1/update.json", name="Test") self.assertEqual(album.id, "2") self.assertEqual(album.name, "Album 2") # self.assertEqual(album.cover.id, "2b") @@ -200,10 +225,11 @@ class TestAlbumUpdate(TestAlbums): class TestAlbumView(TestAlbums): # TODO: cover should be updated to Photo object @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_album_view(self, mock): - mock.return_value = self._return_value(self.TEST_ALBUMS_DICT[1]) - result = self.client.album.view(self.TEST_ALBUMS[0], name="Test") - mock.assert_called_with("/album/1/view.json", name="Test") + def test_album_view(self, mock_get): + """Check that an album can be viewed""" + mock_get.return_value = self._return_value(self.test_albums_dict[1]) + result = self.client.album.view(self.test_albums[0], name="Test") + mock_get.assert_called_with("/album/1/view.json", name="Test") self.assertEqual(result.id, "2") self.assertEqual(result.name, "Album 2") # self.assertEqual(result.cover.id, "2b") @@ -211,10 +237,11 @@ class TestAlbumView(TestAlbums): # TODO: cover should be updated to Photo object @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_album_view_id(self, mock): - mock.return_value = self._return_value(self.TEST_ALBUMS_DICT[1]) + def test_album_view_id(self, mock_get): + """Check that an album can be viewed using its ID""" + mock_get.return_value = self._return_value(self.test_albums_dict[1]) result = self.client.album.view("1", name="Test") - mock.assert_called_with("/album/1/view.json", name="Test") + mock_get.assert_called_with("/album/1/view.json", name="Test") self.assertEqual(result.id, "2") self.assertEqual(result.name, "Album 2") # self.assertEqual(result.cover.id, "2b") @@ -222,11 +249,12 @@ class TestAlbumView(TestAlbums): # TODO: cover should be updated to Photo object @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_album_object_view(self, mock): - mock.return_value = self._return_value(self.TEST_ALBUMS_DICT[1]) - album = self.TEST_ALBUMS[0] + def test_album_object_view(self, mock_get): + """Check that an album can be viewed using the album object directly""" + mock_get.return_value = self._return_value(self.test_albums_dict[1]) + album = self.test_albums[0] album.view(name="Test") - mock.assert_called_with("/album/1/view.json", name="Test") + mock_get.assert_called_with("/album/1/view.json", name="Test") self.assertEqual(album.id, "2") self.assertEqual(album.name, "Album 2") # self.assertEqual(album.cover.id, "2b") diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 46f9f65..07fce91 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -17,59 +17,66 @@ def raise_exception(_): raise TestException() class TestCli(unittest.TestCase): - TEST_FILE = os.path.join("tests", "unit", "data", "test_file.txt") + test_file = os.path.join("tests", "unit", "data", "test_file.txt") @mock.patch.object(openphoto.main, "OpenPhoto") @mock.patch('sys.stdout', new_callable=StringIO) - def test_defaults(self, _, MockOpenPhoto): - get = MockOpenPhoto.return_value.get + def test_defaults(self, _, mock_openphoto): + """Check that the default behaviour is correct""" + get = mock_openphoto.return_value.get main([]) - MockOpenPhoto.assert_called_with(config_file=None) + mock_openphoto.assert_called_with(config_file=None) get.assert_called_with("/photos/list.json", process_response=False) @mock.patch.object(openphoto.main, "OpenPhoto") @mock.patch('sys.stdout', new_callable=StringIO) - def test_config(self, _, MockOpenPhoto): + def test_config(self, _, mock_openphoto): + """Check that a config file can be specified""" main(["--config=test"]) - MockOpenPhoto.assert_called_with(config_file="test") + mock_openphoto.assert_called_with(config_file="test") @mock.patch.object(openphoto.main, "OpenPhoto") @mock.patch('sys.stdout', new_callable=StringIO) - def test_get(self, mock_stdout, MockOpenPhoto): - get = MockOpenPhoto.return_value.get + def test_get(self, mock_stdout, mock_openphoto): + """Check that the get operation is working""" + get = mock_openphoto.return_value.get get.return_value = "Result" main(["-X", "GET", "-h", "test_host", "-e", "test_endpoint", "-F", "field1=1", "-F", "field2=2"]) - MockOpenPhoto.assert_called_with(host="test_host") + mock_openphoto.assert_called_with(host="test_host") get.assert_called_with("test_endpoint", field1="1", field2="2", process_response=False) self.assertEqual(mock_stdout.getvalue(), "Result\n") @mock.patch.object(openphoto.main, "OpenPhoto") @mock.patch('sys.stdout', new_callable=StringIO) - def test_post(self, mock_stdout, MockOpenPhoto): - post = MockOpenPhoto.return_value.post + def test_post(self, mock_stdout, mock_openphoto): + """Check that the post operation is working""" + post = mock_openphoto.return_value.post post.return_value = "Result" main(["-X", "POST", "-h", "test_host", "-e", "test_endpoint", "-F", "field1=1", "-F", "field2=2"]) - MockOpenPhoto.assert_called_with(host="test_host") - post.assert_called_with("test_endpoint", field1="1", field2="2", files={}, - process_response=False) + mock_openphoto.assert_called_with(host="test_host") + post.assert_called_with("test_endpoint", field1="1", field2="2", + files={}, process_response=False) self.assertEqual(mock_stdout.getvalue(), "Result\n") @mock.patch.object(openphoto.main, "OpenPhoto") @mock.patch('sys.stdout', new_callable=StringIO) - def test_post_files(self, _, MockOpenPhoto): - post = MockOpenPhoto.return_value.post - main(["-X", "POST", "-F", "photo=@%s" % self.TEST_FILE]) - # It's not possible to directly compare the file object, so check it manually + def test_post_files(self, _, mock_openphoto): + """Check that files are posted correctly""" + post = mock_openphoto.return_value.post + main(["-X", "POST", "-F", "photo=@%s" % self.test_file]) + # It's not possible to directly compare the file object, + # so check it manually files = post.call_args[1]["files"] self.assertEqual(files.keys(), ["photo"]) - self.assertEqual(files["photo"].name, self.TEST_FILE) + self.assertEqual(files["photo"].name, self.test_file) @mock.patch.object(sys, "exit", raise_exception) @mock.patch('sys.stderr', new_callable=StringIO) def test_unknown_arg(self, mock_stderr): + """Check that an unknown argument produces an error""" with self.assertRaises(TestException): main(["hello"]) self.assertIn("error: Unknown argument", mock_stderr.getvalue()) @@ -77,6 +84,7 @@ class TestCli(unittest.TestCase): @mock.patch.object(sys, "exit", raise_exception) @mock.patch('sys.stderr', new_callable=StringIO) def test_unknown_option(self, mock_stderr): + """Check that an unknown option produces an error""" with self.assertRaises(TestException): main(["--hello"]) self.assertIn("error: no such option", mock_stderr.getvalue()) @@ -84,23 +92,27 @@ class TestCli(unittest.TestCase): @mock.patch.object(sys, "exit", raise_exception) @mock.patch('sys.stdout', new_callable=StringIO) def test_unknown_config(self, mock_stdout): + """Check that an unknown config file produces an error""" with self.assertRaises(TestException): main(["--config=this_config_doesnt_exist"]) self.assertIn("No such file or directory", mock_stdout.getvalue()) - self.assertIn("You must create a configuration file", mock_stdout.getvalue()) + self.assertIn("You must create a configuration file", + mock_stdout.getvalue()) self.assertIn("To get your credentials", mock_stdout.getvalue()) @mock.patch.object(openphoto.main, "OpenPhoto") @mock.patch('sys.stdout', new_callable=StringIO) def test_verbose(self, mock_stdout, _): + """Check that the verbose option is working""" main(["-v"]) self.assertIn("Method: GET", mock_stdout.getvalue()) self.assertIn("Endpoint: /photos/list.json", mock_stdout.getvalue()) @mock.patch.object(openphoto.main, "OpenPhoto") @mock.patch('sys.stdout', new_callable=StringIO) - def test_pretty_print(self, mock_stdout, MockOpenPhoto): - get = MockOpenPhoto.return_value.get + def test_pretty_print(self, mock_stdout, mock_openphoto): + """Check that the pretty-print option is working""" + get = mock_openphoto.return_value.get get.return_value = '{"test":1}' main(["-p"]) self.assertEqual(mock_stdout.getvalue(), '{\n "test":1\n}\n') diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 6e37ab2..9016e20 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -29,11 +29,12 @@ class TestConfig(unittest.TestCase): @staticmethod def create_config(config_file, host): + """Create a dummy config file""" with open(os.path.join(CONFIG_PATH, config_file), "w") as conf: conf.write("host = %s\n" % host) conf.write("# Comment\n\n") conf.write("consumerKey = \"%s_consumer_key\"\n" % config_file) - conf.write("\"consumerSecret\" = %s_consumer_secret\n" % config_file) + conf.write("\"consumerSecret\"= %s_consumer_secret\n" % config_file) conf.write("'token'=%s_token\n" % config_file) conf.write("tokenSecret = '%s_token_secret'\n" % config_file) diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py index 971dd90..ba092b7 100644 --- a/tests/unit/test_http.py +++ b/tests/unit/test_http.py @@ -10,25 +10,28 @@ except ImportError: import openphoto class TestHttp(unittest.TestCase): - TEST_HOST = "test.example.com" - TEST_ENDPOINT = "test.json" - TEST_URI = "http://%s/%s" % (TEST_HOST, TEST_ENDPOINT) - TEST_DATA = {"message": "Test Message", + test_host = "test.example.com" + test_endpoint = "test.json" + test_uri = "http://%s/%s" % (test_host, test_endpoint) + test_data = {"message": "Test Message", "code": 200, "result": "Test Result"} - TEST_OAUTH = {"consumer_key": "dummy", + test_oauth = {"consumer_key": "dummy", "consumer_secret": "dummy", "token": "dummy", "token_secret": "dummy"} - TEST_FILE = os.path.join("tests", "unit", "data", "test_file.txt") + test_file = os.path.join("tests", "unit", "data", "test_file.txt") def setUp(self): - self.client = openphoto.OpenPhoto(host=self.TEST_HOST, **self.TEST_OAUTH) + self.client = openphoto.OpenPhoto(host=self.test_host, + **self.test_oauth) - def _register_uri(self, method, uri=TEST_URI, data=TEST_DATA, body=None, + def _register_uri(self, method, uri=test_uri, data=None, body=None, **kwds): """Convenience wrapper around httpretty.register_uri""" + if data is None: + data = self.test_data if body is None: body = json.dumps(data) httpretty.register_uri(method, uri=uri, body=body, **kwds) @@ -39,72 +42,82 @@ class TestHttp(unittest.TestCase): return httpretty.httpretty.last_request def test_attributes(self): - self.assertEqual(self.client.host, self.TEST_HOST) - self.assertEqual(self.client.config.host, self.TEST_HOST) + """Check that the host attribute has been set correctly""" + self.assertEqual(self.client.host, self.test_host) + self.assertEqual(self.client.config.host, self.test_host) @httpretty.activate def test_get_with_parameters(self): + """Check that the get method accepts parameters correctly""" self._register_uri(httpretty.GET) - response = self.client.get(self.TEST_ENDPOINT, + response = self.client.get(self.test_endpoint, foo="bar", spam="eggs") self.assertIn("OAuth", self._last_request().headers["authorization"]) self.assertEqual(self._last_request().querystring["foo"], ["bar"]) self.assertEqual(self._last_request().querystring["spam"], ["eggs"]) - self.assertEqual(response, self.TEST_DATA) - self.assertEqual(self.client.last_url, self.TEST_URI) - self.assertEqual(self.client.last_params, {"foo": "bar", "spam": "eggs"}) - self.assertEqual(self.client.last_response.json(), self.TEST_DATA) + self.assertEqual(response, self.test_data) + self.assertEqual(self.client.last_url, self.test_uri) + self.assertEqual(self.client.last_params, {"foo": "bar", + "spam": "eggs"}) + self.assertEqual(self.client.last_response.json(), self.test_data) @httpretty.activate def test_post_with_parameters(self): + """Check that the post method accepts parameters correctly""" self._register_uri(httpretty.POST) - response = self.client.post(self.TEST_ENDPOINT, + response = self.client.post(self.test_endpoint, foo="bar", spam="eggs") self.assertEqual(self._last_request().body, "foo=bar&spam=eggs") - self.assertEqual(response, self.TEST_DATA) - self.assertEqual(self.client.last_url, self.TEST_URI) - self.assertEqual(self.client.last_params, {"foo": "bar", "spam": "eggs"}) - self.assertEqual(self.client.last_response.json(), self.TEST_DATA) + self.assertEqual(response, self.test_data) + self.assertEqual(self.client.last_url, self.test_uri) + self.assertEqual(self.client.last_params, {"foo": "bar", + "spam": "eggs"}) + self.assertEqual(self.client.last_response.json(), self.test_data) @httpretty.activate def test_get_without_oauth(self): - self.client = openphoto.OpenPhoto(host=self.TEST_HOST) + """Check that the get method works without OAuth parameters""" + self.client = openphoto.OpenPhoto(host=self.test_host) self._register_uri(httpretty.GET) - response = self.client.get(self.TEST_ENDPOINT) + response = self.client.get(self.test_endpoint) self.assertNotIn("authorization", self._last_request().headers) - self.assertEqual(response, self.TEST_DATA) + self.assertEqual(response, self.test_data) @httpretty.activate - def test_post_without_oauth_raises_exception(self): - self.client = openphoto.OpenPhoto(host=self.TEST_HOST) + def test_post_without_oauth(self): + """Check that the post method fails without OAuth parameters""" + self.client = openphoto.OpenPhoto(host=self.test_host) self._register_uri(httpretty.POST) with self.assertRaises(openphoto.OpenPhotoError): - self.client.post(self.TEST_ENDPOINT) + self.client.post(self.test_endpoint) @httpretty.activate def test_get_without_response_processing(self): + """Check that the get method works with response processing disabled""" self._register_uri(httpretty.GET) - response = self.client.get(self.TEST_ENDPOINT, process_response=False) - self.assertEqual(response, json.dumps(self.TEST_DATA)) + response = self.client.get(self.test_endpoint, process_response=False) + self.assertEqual(response, json.dumps(self.test_data)) @httpretty.activate def test_post_without_response_processing(self): + """Check that the post method works with response processing disabled""" self._register_uri(httpretty.POST) - response = self.client.post(self.TEST_ENDPOINT, process_response=False) - self.assertEqual(response, json.dumps(self.TEST_DATA)) + response = self.client.post(self.test_endpoint, process_response=False) + self.assertEqual(response, json.dumps(self.test_data)) @httpretty.activate def test_get_parameter_processing(self): + """Check that the parameter processing function is working""" self._register_uri(httpretty.GET) photo = openphoto.objects.Photo(None, {"id": "photo_id"}) album = openphoto.objects.Album(None, {"id": "album_id"}) tag = openphoto.objects.Tag(None, {"id": "tag_id"}) - self.client.get(self.TEST_ENDPOINT, + self.client.get(self.test_endpoint, photo=photo, album=album, tag=tag, list_=[photo, album, tag], boolean=True, unicode_="\xfcmlaut") - params=self._last_request().querystring + params = self._last_request().querystring self.assertEqual(params["photo"], ["photo_id"]) self.assertEqual(params["album"], ["album_id"]) self.assertEqual(params["tag"], ["tag_id"]) @@ -114,28 +127,31 @@ class TestHttp(unittest.TestCase): @httpretty.activate def test_get_with_api_version(self): - self.client = openphoto.OpenPhoto(host=self.TEST_HOST, api_version=1) + """Check that an API version can be specified for the get method""" + self.client = openphoto.OpenPhoto(host=self.test_host, api_version=1) self._register_uri(httpretty.GET, - uri="http://%s/v1/%s" % (self.TEST_HOST, - self.TEST_ENDPOINT)) - self.client.get(self.TEST_ENDPOINT) + uri="http://%s/v1/%s" % (self.test_host, + self.test_endpoint)) + self.client.get(self.test_endpoint) @httpretty.activate def test_post_with_api_version(self): - self.client = openphoto.OpenPhoto(host=self.TEST_HOST, api_version=1, - **self.TEST_OAUTH) + """Check that an API version can be specified for the post method""" + self.client = openphoto.OpenPhoto(host=self.test_host, api_version=1, + **self.test_oauth) self._register_uri(httpretty.POST, - uri="http://%s/v1/%s" % (self.TEST_HOST, - self.TEST_ENDPOINT)) - self.client.post(self.TEST_ENDPOINT) + uri="http://%s/v1/%s" % (self.test_host, + self.test_endpoint)) + self.client.post(self.test_endpoint) @httpretty.activate def test_post_file(self): + """Check that a file can be posted""" self._register_uri(httpretty.POST) - with open(self.TEST_FILE, 'rb') as in_file: - response = self.client.post(self.TEST_ENDPOINT, + with open(self.test_file, 'rb') as in_file: + response = self.client.post(self.test_endpoint, files={"file": in_file}) - self.assertEqual(response, self.TEST_DATA) + self.assertEqual(response, self.test_data) body = self._last_request().body self.assertIn("Content-Disposition: form-data; "+ "name=\"file\"; filename=\"test_file.txt\"", body) @@ -144,9 +160,13 @@ class TestHttp(unittest.TestCase): @httpretty.activate def test_post_file_parameters_are_sent_as_querystring(self): + """ + Check that parameters are send as a query string + when a file is posted + """ self._register_uri(httpretty.POST) - with open(self.TEST_FILE, 'rb') as in_file: - response = self.client.post(self.TEST_ENDPOINT, foo="bar", + with open(self.test_file, 'rb') as in_file: + response = self.client.post(self.test_endpoint, foo="bar", files={"file": in_file}) - self.assertEqual(response, self.TEST_DATA) + self.assertEqual(response, self.test_data) self.assertEqual(self._last_request().querystring["foo"], ["bar"]) diff --git a/tests/unit/test_http_errors.py b/tests/unit/test_http_errors.py index b25f78c..ddd25ec 100644 --- a/tests/unit/test_http_errors.py +++ b/tests/unit/test_http_errors.py @@ -9,25 +9,26 @@ except ImportError: import openphoto class TestHttpErrors(unittest.TestCase): - TEST_HOST = "test.example.com" - TEST_ENDPOINT = "test.json" - TEST_URI = "http://%s/%s" % (TEST_HOST, TEST_ENDPOINT) - TEST_DATA = {"message": "Test Message", + test_host = "test.example.com" + test_endpoint = "test.json" + test_uri = "http://%s/%s" % (test_host, test_endpoint) + test_data = {"message": "Test Message", "code": 200, "result": "Test Result"} - TEST_OAUTH = {"consumer_key": "dummy", + test_oauth = {"consumer_key": "dummy", "consumer_secret": "dummy", "token": "dummy", "token_secret": "dummy"} def setUp(self): - self.client = openphoto.OpenPhoto(host=self.TEST_HOST, **self.TEST_OAUTH) + self.client = openphoto.OpenPhoto(host=self.test_host, + **self.test_oauth) - def _register_uri(self, method, uri=TEST_URI, + def _register_uri(self, method, uri=test_uri, data=None, body=None, status=200, **kwds): """Convenience wrapper around httpretty.register_uri""" if data is None: - data = self.TEST_DATA + data = self.test_data # Set the JSON return code to match the HTTP status data["code"] = status if body is None: @@ -36,98 +37,154 @@ class TestHttpErrors(unittest.TestCase): **kwds) @httpretty.activate - def test_get_with_error_status_raises_openphoto_exception(self): + def test_get_with_error_status(self): + """ + Check that an error status causes the get method + to raise an exception + """ self._register_uri(httpretty.GET, status=500) with self.assertRaises(openphoto.OpenPhotoError): - self.client.get(self.TEST_ENDPOINT) + self.client.get(self.test_endpoint) @httpretty.activate - def test_post_with_error_status_raises_openphoto_exception(self): + def test_post_with_error_status(self): + """ + Check that an error status causes the post method + to raise an exception + """ self._register_uri(httpretty.POST, status=500) with self.assertRaises(openphoto.OpenPhotoError): - self.client.post(self.TEST_ENDPOINT) + self.client.post(self.test_endpoint) # TODO: 404 status should raise 404 error, even if JSON is valid @unittest.expectedFailure @httpretty.activate - def test_get_with_404_status_raises_404_exception(self): + def test_get_with_404_status(self): + """ + Check that a 404 status causes the get method + to raise a 404 exception + """ self._register_uri(httpretty.GET, status=404) with self.assertRaises(openphoto.OpenPhoto404Error): - response = self.client.get(self.TEST_ENDPOINT) + self.client.get(self.test_endpoint) # TODO: 404 status should raise 404 error, even if JSON is valid @unittest.expectedFailure @httpretty.activate - def test_post_with_404_status_raises_404_exception(self): + def test_post_with_404_status(self): + """ + Check that a 404 status causes the post method + to raise a 404 exception + """ self._register_uri(httpretty.POST, status=404) with self.assertRaises(openphoto.OpenPhoto404Error): - response = self.client.post(self.TEST_ENDPOINT) + self.client.post(self.test_endpoint) @httpretty.activate - def test_get_with_invalid_json_raises_exception(self): + def test_get_with_invalid_json(self): + """ + Check that invalid JSON causes the get method to + raise an exception + """ self._register_uri(httpretty.GET, body="Invalid JSON") with self.assertRaises(ValueError): - self.client.get(self.TEST_ENDPOINT) + self.client.get(self.test_endpoint) @httpretty.activate - def test_post_with_invalid_json_raises_exception(self): + def test_post_with_invalid_json(self): + """ + Check that invalid JSON causes the post method to + raise an exception + """ self._register_uri(httpretty.POST, body="Invalid JSON") with self.assertRaises(ValueError): - self.client.post(self.TEST_ENDPOINT) + self.client.post(self.test_endpoint) @httpretty.activate - def test_get_with_error_status_and_invalid_json_raises_openphoto_exception(self): + def test_get_with_error_status_and_invalid_json(self): + """ + Check that invalid JSON causes the get method to raise an exception, + even with an error status is returned + """ self._register_uri(httpretty.GET, body="Invalid JSON", status=500) with self.assertRaises(openphoto.OpenPhotoError): - response = self.client.get(self.TEST_ENDPOINT) + self.client.get(self.test_endpoint) @httpretty.activate - def test_post_with_error_status_and_invalid_json_raises_openphoto_exception(self): + def test_post_with_error_status_and_invalid_json(self): + """ + Check that invalid JSON causes the post method to raise an exception, + even with an error status is returned + """ self._register_uri(httpretty.POST, body="Invalid JSON", status=500) with self.assertRaises(openphoto.OpenPhotoError): - response = self.client.post(self.TEST_ENDPOINT) + self.client.post(self.test_endpoint) @httpretty.activate - def test_get_with_404_status_and_invalid_json_raises_404_exception(self): + def test_get_with_404_status_and_invalid_json(self): + """ + Check that invalid JSON causes the get method to raise an exception, + even with a 404 status is returned + """ self._register_uri(httpretty.GET, body="Invalid JSON", status=404) with self.assertRaises(openphoto.OpenPhoto404Error): - response = self.client.get(self.TEST_ENDPOINT) + self.client.get(self.test_endpoint) @httpretty.activate - def test_post_with_404_status_and_invalid_json_raises_404_exception(self): + def test_post_with_404_status_and_invalid_json(self): + """ + Check that invalid JSON causes the post method to raise an exception, + even with a 404 status is returned + """ self._register_uri(httpretty.POST, body="Invalid JSON", status=404) with self.assertRaises(openphoto.OpenPhoto404Error): - response = self.client.post(self.TEST_ENDPOINT) + self.client.post(self.test_endpoint) @httpretty.activate - def test_get_with_duplicate_status_raises_duplicate_exception(self): + def test_get_with_duplicate_status(self): + """ + Check that a get with a duplicate status + raises a duplicate exception + """ data = {"message": "This photo already exists", "code": 409} self._register_uri(httpretty.GET, data=data, status=409) with self.assertRaises(openphoto.OpenPhotoDuplicateError): - response = self.client.get(self.TEST_ENDPOINT) + self.client.get(self.test_endpoint) @httpretty.activate - def test_post_with_duplicate_status_raises_duplicate_exception(self): + def test_post_with_duplicate_status(self): + """ + Check that a post with a duplicate status + raises a duplicate exception + """ data = {"message": "This photo already exists", "code": 409} self._register_uri(httpretty.POST, data=data, status=409) with self.assertRaises(openphoto.OpenPhotoDuplicateError): - response = self.client.post(self.TEST_ENDPOINT) + self.client.post(self.test_endpoint) # TODO: Status code mismatch should raise an exception @unittest.expectedFailure @httpretty.activate - def test_get_with_status_code_mismatch_raises_openphoto_exception(self): + def test_get_with_status_code_mismatch(self): + """ + Check that an exception is raised if a get returns a + status code that doesn't match the JSON code + """ data = {"message": "Test Message", "code": 200} self._register_uri(httpretty.GET, data=data, status=202) with self.assertRaises(openphoto.OpenPhotoError): - response = self.client.get(self.TEST_ENDPOINT) + self.client.get(self.test_endpoint) # TODO: Status code mismatch should raise an exception @unittest.expectedFailure @httpretty.activate - def test_post_with_status_code_mismatch_raises_openphoto_exception(self): + def test_post_with_status_code_mismatch(self): + """ + Check that an exception is raised if a post returns a + status code that doesn't match the JSON code + """ data = {"message": "Test Message", "code": 200} self._register_uri(httpretty.POST, data=data, status=202) with self.assertRaises(openphoto.OpenPhotoError): - response = self.client.post(self.TEST_ENDPOINT) + self.client.post(self.test_endpoint) diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index 932cf1a..8e55664 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -10,16 +10,16 @@ except ImportError: import openphoto class TestPhotos(unittest.TestCase): - TEST_HOST = "test.example.com" - TEST_FILE = os.path.join("tests", "unit", "data", "test_file.txt") - TEST_PHOTOS_DICT = [{"id": "1a", "tags": ["tag1", "tag2"], + test_host = "test.example.com" + test_file = os.path.join("tests", "unit", "data", "test_file.txt") + test_photos_dict = [{"id": "1a", "tags": ["tag1", "tag2"], "totalPages": 1, "totalRows": 2}, {"id": "2b", "tags": ["tag3", "tag4"], "totalPages": 1, "totalRows": 2}] def setUp(self): - self.client = openphoto.OpenPhoto(host=self.TEST_HOST) - self.TEST_PHOTOS = [openphoto.objects.Photo(self.client, photo) - for photo in self.TEST_PHOTOS_DICT] + self.client = openphoto.OpenPhoto(host=self.test_host) + self.test_photos = [openphoto.objects.Photo(self.client, photo) + for photo in self.test_photos_dict] @staticmethod def _return_value(result, message="", code=200): @@ -27,11 +27,12 @@ class TestPhotos(unittest.TestCase): class TestPhotosList(TestPhotos): @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photos_list(self, mock): - mock.return_value = self._return_value(self.TEST_PHOTOS_DICT) + def test_photos_list(self, mock_get): + """Check that the photo list is returned correctly""" + mock_get.return_value = self._return_value(self.test_photos_dict) result = self.client.photos.list() - mock.assert_called_with("/photos/list.json") + mock_get.assert_called_with("/photos/list.json") self.assertEqual(len(result), 2) self.assertEqual(result[0].id, "1a") self.assertEqual(result[0].tags, ["tag1", "tag2"]) @@ -42,80 +43,99 @@ class TestPhotosUpdate(TestPhotos): # TODO: photos.update should accept a list of Photo objects @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photos_update(self, mock): - mock.return_value = self._return_value(True) - result = self.client.photos.update(self.TEST_PHOTOS, title="Test") - mock.assert_called_with("/photos/update.json", + def test_photos_update(self, mock_post): + """Check that multiple photos can be updated""" + mock_post.return_value = self._return_value(True) + result = self.client.photos.update(self.test_photos, title="Test") + mock_post.assert_called_with("/photos/update.json", ids=["1a", "2b"], title="Test") self.assertEqual(result, True) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photos_update_ids(self, mock): - mock.return_value = self._return_value(True) + def test_photos_update_ids(self, mock_post): + """Check that multiple photos can be updated using their IDs""" + mock_post.return_value = self._return_value(True) result = self.client.photos.update(["1a", "2b"], title="Test") - mock.assert_called_with("/photos/update.json", + mock_post.assert_called_with("/photos/update.json", ids=["1a", "2b"], title="Test") self.assertEqual(result, True) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photos_update_failure_raises_exception(self, mock): - mock.return_value = self._return_value(False) + def test_photos_update_failure(self, mock_post): + """ + Check that an exception is raised if multiple photos + cannot be updated + """ + mock_post.return_value = self._return_value(False) with self.assertRaises(openphoto.OpenPhotoError): - self.client.photos.update(self.TEST_PHOTOS, title="Test") + self.client.photos.update(self.test_photos, title="Test") class TestPhotosDelete(TestPhotos): # TODO: photos.delete should accept a list of Photo objects @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photos_delete(self, mock): - mock.return_value = self._return_value(True) - result = self.client.photos.delete(self.TEST_PHOTOS) - mock.assert_called_with("/photos/delete.json", ids=["1a", "2b"]) + def test_photos_delete(self, mock_post): + """Check that multiple photos can be deleted""" + mock_post.return_value = self._return_value(True) + result = self.client.photos.delete(self.test_photos) + mock_post.assert_called_with("/photos/delete.json", ids=["1a", "2b"]) self.assertEqual(result, True) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photos_delete_ids(self, mock): - mock.return_value = self._return_value(True) + def test_photos_delete_ids(self, mock_post): + """Check that multiple photos can be deleted using their IDs""" + mock_post.return_value = self._return_value(True) result = self.client.photos.delete(["1a", "2b"]) - mock.assert_called_with("/photos/delete.json", ids=["1a", "2b"]) + mock_post.assert_called_with("/photos/delete.json", ids=["1a", "2b"]) self.assertEqual(result, True) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photos_delete_failure_raises_exception(self, mock): - mock.return_value = self._return_value(False) + def test_photos_delete_failure(self, mock_post): + """ + Check that an exception is raised if multiple photos + cannot be deleted + """ + mock_post.return_value = self._return_value(False) with self.assertRaises(openphoto.OpenPhotoError): - self.client.photos.delete(self.TEST_PHOTOS) + self.client.photos.delete(self.test_photos) class TestPhotoDelete(TestPhotos): @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_delete(self, mock): - mock.return_value = self._return_value(True) - result = self.client.photo.delete(self.TEST_PHOTOS[0]) - mock.assert_called_with("/photo/1a/delete.json") + def test_photo_delete(self, mock_post): + """Check that a photo can be deleted""" + mock_post.return_value = self._return_value(True) + result = self.client.photo.delete(self.test_photos[0]) + mock_post.assert_called_with("/photo/1a/delete.json") self.assertEqual(result, True) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_delete_id(self, mock): - mock.return_value = self._return_value(True) + def test_photo_delete_id(self, mock_post): + """Check that a photo can be deleted using its ID""" + mock_post.return_value = self._return_value(True) result = self.client.photo.delete("1a") - mock.assert_called_with("/photo/1a/delete.json") + mock_post.assert_called_with("/photo/1a/delete.json") self.assertEqual(result, True) # TODO: photo.delete should raise exception on failure @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_delete_failure_raises_exception(self, mock): - mock.return_value = self._return_value(False) + def test_photo_delete_failure(self, mock_post): + """Check that an exception is raised if a photo cannot be deleted""" + mock_post.return_value = self._return_value(False) with self.assertRaises(openphoto.OpenPhotoError): - self.client.photo.delete(self.TEST_PHOTOS[0]) + self.client.photo.delete(self.test_photos[0]) # TODO: after deleting object fields, name and id should be set to None @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_object_delete(self, mock): - mock.return_value = self._return_value(True) - photo = self.TEST_PHOTOS[0] + def test_photo_object_delete(self, mock_post): + """ + Check that a photo can be deleted when using + the photo object directly + """ + mock_post.return_value = self._return_value(True) + photo = self.test_photos[0] result = photo.delete() - mock.assert_called_with("/photo/1a/delete.json") + mock_post.assert_called_with("/photo/1a/delete.json") self.assertEqual(result, True) self.assertEqual(photo.get_fields(), {}) # self.assertEqual(photo.id, None) @@ -123,245 +143,298 @@ class TestPhotoDelete(TestPhotos): # TODO: photo.delete should raise exception on failure @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_object_delete_failure_raises_exception(self, mock): - mock.return_value = self._return_value(False) + def test_photo_object_delete_failure(self, mock_post): + """ + Check that an exception is raised if a photo cannot be deleted + when using the photo object directly + """ + mock_post.return_value = self._return_value(False) with self.assertRaises(openphoto.OpenPhotoError): - self.TEST_PHOTOS[0].delete() + self.test_photos[0].delete() class TestPhotoEdit(TestPhotos): @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photo_edit(self, mock): - mock.return_value = self._return_value({"markup": ""}) - result = self.client.photo.edit(self.TEST_PHOTOS[0]) - mock.assert_called_with("/photo/1a/edit.json") + def test_photo_edit(self, mock_get): + """Check that a the photo edit endpoint is working""" + mock_get.return_value = self._return_value({"markup": ""}) + result = self.client.photo.edit(self.test_photos[0]) + mock_get.assert_called_with("/photo/1a/edit.json") self.assertEqual(result, "") @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photo_edit_id(self, mock): - mock.return_value = self._return_value({"markup": ""}) + def test_photo_edit_id(self, mock_get): + """Check that a the photo edit endpoint is working when using an ID""" + mock_get.return_value = self._return_value({"markup": ""}) result = self.client.photo.edit("1a") - mock.assert_called_with("/photo/1a/edit.json") + mock_get.assert_called_with("/photo/1a/edit.json") self.assertEqual(result, "") @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photo_object_edit(self, mock): - mock.return_value = self._return_value({"markup": ""}) - result = self.TEST_PHOTOS[0].edit() - mock.assert_called_with("/photo/1a/edit.json") + def test_photo_object_edit(self, mock_get): + """ + Check that a the photo edit endpoint is working + when using the photo object directly + """ + mock_get.return_value = self._return_value({"markup": ""}) + result = self.test_photos[0].edit() + mock_get.assert_called_with("/photo/1a/edit.json") self.assertEqual(result, "") class TestPhotoReplace(TestPhotos): @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_replace(self, mock): + def test_photo_replace(self, _): + """ If photo.replace gets implemented, write a test! """ with self.assertRaises(NotImplementedError): - self.client.photo.replace(self.TEST_PHOTOS[0], self.TEST_FILE) + self.client.photo.replace(self.test_photos[0], self.test_file) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_replace_id(self, mock): + def test_photo_replace_id(self, _): + """ If photo.replace gets implemented, write a test! """ with self.assertRaises(NotImplementedError): - self.client.photo.replace("1a", self.TEST_FILE) + self.client.photo.replace("1a", self.test_file) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_object_replace(self, mock): + def test_photo_object_replace(self, _): + """ If photo.replace gets implemented, write a test! """ with self.assertRaises(NotImplementedError): - self.TEST_PHOTOS[0].replace(self.TEST_FILE) + self.test_photos[0].replace(self.test_file) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_replace_encoded(self, mock): + def test_photo_replace_encoded(self, _): + """ If photo.replace_encoded gets implemented, write a test! """ with self.assertRaises(NotImplementedError): - self.client.photo.replace_encoded(self.TEST_PHOTOS[0], self.TEST_FILE) + self.client.photo.replace_encoded(self.test_photos[0], + self.test_file) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_replace_encoded_id(self, mock): + def test_photo_replace_encoded_id(self, _): + """ If photo.replace_encoded gets implemented, write a test! """ with self.assertRaises(NotImplementedError): - self.client.photo.replace_encoded("1a", self.TEST_FILE) + self.client.photo.replace_encoded("1a", self.test_file) # TODO: replace_encoded parameter should be called photo_file, # not encoded_photo @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_object_replace_encoded(self, mock): + def test_photo_object_replace_encoded(self, _): + """ If photo.replace_encoded gets implemented, write a test! """ with self.assertRaises(NotImplementedError): - self.TEST_PHOTOS[0].replace_encoded(photo_file=self.TEST_FILE) + self.test_photos[0].replace_encoded(photo_file=self.test_file) class TestPhotoUpdate(TestPhotos): @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_update(self, mock): - mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) - result = self.client.photo.update(self.TEST_PHOTOS[0], title="Test") - mock.assert_called_with("/photo/1a/update.json", title="Test") - self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[1]) + def test_photo_update(self, mock_post): + """Check that a photo can be updated""" + mock_post.return_value = self._return_value(self.test_photos_dict[1]) + result = self.client.photo.update(self.test_photos[0], title="Test") + mock_post.assert_called_with("/photo/1a/update.json", title="Test") + self.assertEqual(result.get_fields(), self.test_photos_dict[1]) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_update_id(self, mock): - mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) + def test_photo_update_id(self, mock_post): + """Check that a photo can be updated using its ID""" + mock_post.return_value = self._return_value(self.test_photos_dict[1]) result = self.client.photo.update("1a", title="Test") - mock.assert_called_with("/photo/1a/update.json", title="Test") - self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[1]) + mock_post.assert_called_with("/photo/1a/update.json", title="Test") + self.assertEqual(result.get_fields(), self.test_photos_dict[1]) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_object_update(self, mock): - mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) - photo = self.TEST_PHOTOS[0] + def test_photo_object_update(self, mock_post): + """ + Check that a photo can be updated + when using the photo object directly + """ + mock_post.return_value = self._return_value(self.test_photos_dict[1]) + photo = self.test_photos[0] photo.update(title="Test") - mock.assert_called_with("/photo/1a/update.json", title="Test") - self.assertEqual(photo.get_fields(), self.TEST_PHOTOS_DICT[1]) + mock_post.assert_called_with("/photo/1a/update.json", title="Test") + self.assertEqual(photo.get_fields(), self.test_photos_dict[1]) class TestPhotoView(TestPhotos): @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photo_view(self, mock): - mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) - result = self.client.photo.view(self.TEST_PHOTOS[0], returnSizes="20x20") - mock.assert_called_with("/photo/1a/view.json", returnSizes="20x20") - self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[1]) + def test_photo_view(self, mock_get): + """Check that a photo can be viewed""" + mock_get.return_value = self._return_value(self.test_photos_dict[1]) + result = self.client.photo.view(self.test_photos[0], + returnSizes="20x20") + mock_get.assert_called_with("/photo/1a/view.json", returnSizes="20x20") + self.assertEqual(result.get_fields(), self.test_photos_dict[1]) @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photo_view_id(self, mock): - mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) + def test_photo_view_id(self, mock_get): + """Check that a photo can be viewed using its ID""" + mock_get.return_value = self._return_value(self.test_photos_dict[1]) result = self.client.photo.view("1a", returnSizes="20x20") - mock.assert_called_with("/photo/1a/view.json", returnSizes="20x20") - self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[1]) + mock_get.assert_called_with("/photo/1a/view.json", returnSizes="20x20") + self.assertEqual(result.get_fields(), self.test_photos_dict[1]) @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photo_object_view(self, mock): - mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) - photo = self.TEST_PHOTOS[0] + def test_photo_object_view(self, mock_get): + """ + Check that a photo can be viewed + when using the photo object directly + """ + mock_get.return_value = self._return_value(self.test_photos_dict[1]) + photo = self.test_photos[0] photo.view(returnSizes="20x20") - mock.assert_called_with("/photo/1a/view.json", returnSizes="20x20") - self.assertEqual(photo.get_fields(), self.TEST_PHOTOS_DICT[1]) + mock_get.assert_called_with("/photo/1a/view.json", returnSizes="20x20") + self.assertEqual(photo.get_fields(), self.test_photos_dict[1]) class TestPhotoUpload(TestPhotos): @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_upload(self, mock): - mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[0]) - result = self.client.photo.upload(self.TEST_FILE, title="Test") + def test_photo_upload(self, mock_post): + """Check that a photo can be uploaded""" + mock_post.return_value = self._return_value(self.test_photos_dict[0]) + result = self.client.photo.upload(self.test_file, title="Test") # It's not possible to compare the file object, # so check each parameter individually - endpoint = mock.call_args[0] - title = mock.call_args[1]["title"] - files = mock.call_args[1]["files"] + endpoint = mock_post.call_args[0] + title = mock_post.call_args[1]["title"] + files = mock_post.call_args[1]["files"] self.assertEqual(endpoint, ("/photo/upload.json",)) self.assertEqual(title, "Test") self.assertIn("photo", files) - self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[0]) + self.assertEqual(result.get_fields(), self.test_photos_dict[0]) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_upload_encoded(self, mock): - encoded_file = base64.b64encode(open(self.TEST_FILE, "rb").read()) - mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[0]) - result = self.client.photo.upload_encoded(self.TEST_FILE, title="Test") - mock.assert_called_with("/photo/upload.json", - photo=encoded_file, title="Test") - self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[0]) + def test_photo_upload_encoded(self, mock_post): + """Check that a photo can be uploaded using Base64 encoding""" + encoded_file = base64.b64encode(open(self.test_file, "rb").read()) + mock_post.return_value = self._return_value(self.test_photos_dict[0]) + result = self.client.photo.upload_encoded(self.test_file, title="Test") + mock_post.assert_called_with("/photo/upload.json", + photo=encoded_file, title="Test") + self.assertEqual(result.get_fields(), self.test_photos_dict[0]) class TestPhotoDynamicUrl(TestPhotos): @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photo_dynamic_url(self, mock): + def test_photo_dynamic_url(self, _): + """ If photo.dynamic_url gets implemented, write a test! """ with self.assertRaises(NotImplementedError): - self.client.photo.dynamic_url(self.TEST_PHOTOS[0]) + self.client.photo.dynamic_url(self.test_photos[0]) @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photo_dynamic_url_id(self, mock): + def test_photo_dynamic_url_id(self, _): + """ If photo.dynamic_url gets implemented, write a test! """ with self.assertRaises(NotImplementedError): self.client.photo.dynamic_url("1a") @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photo_object_dynamic_url(self, mock): + def test_photo_object_dynamic_url(self, _): + """ If photo.dynamic_url gets implemented, write a test! """ with self.assertRaises(NotImplementedError): - self.TEST_PHOTOS[0].dynamic_url() + self.test_photos[0].dynamic_url() class TestPhotoNextPrevious(TestPhotos): @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photo_next_previous(self, mock): - mock.return_value = self._return_value( - {"next": [self.TEST_PHOTOS_DICT[0]], - "previous": [self.TEST_PHOTOS_DICT[1]]}) - result = self.client.photo.next_previous(self.TEST_PHOTOS[0]) - mock.assert_called_with("/photo/1a/nextprevious.json") + def test_photo_next_previous(self, mock_get): + """Check that the next/previous photos are returned""" + mock_get.return_value = self._return_value( + {"next": [self.test_photos_dict[0]], + "previous": [self.test_photos_dict[1]]}) + result = self.client.photo.next_previous(self.test_photos[0]) + mock_get.assert_called_with("/photo/1a/nextprevious.json") self.assertEqual(result["next"][0].get_fields(), - self.TEST_PHOTOS_DICT[0]) + self.test_photos_dict[0]) self.assertEqual(result["previous"][0].get_fields(), - self.TEST_PHOTOS_DICT[1]) + self.test_photos_dict[1]) @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photo_next_previous_id(self, mock): - mock.return_value = self._return_value( - {"next": [self.TEST_PHOTOS_DICT[0]], - "previous": [self.TEST_PHOTOS_DICT[1]]}) + def test_photo_next_previous_id(self, mock_get): + """ + Check that the next/previous photos are returned + when using the photo ID + """ + mock_get.return_value = self._return_value( + {"next": [self.test_photos_dict[0]], + "previous": [self.test_photos_dict[1]]}) result = self.client.photo.next_previous("1a") - mock.assert_called_with("/photo/1a/nextprevious.json") + mock_get.assert_called_with("/photo/1a/nextprevious.json") self.assertEqual(result["next"][0].get_fields(), - self.TEST_PHOTOS_DICT[0]) + self.test_photos_dict[0]) self.assertEqual(result["previous"][0].get_fields(), - self.TEST_PHOTOS_DICT[1]) + self.test_photos_dict[1]) @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photo_object_next_previous(self, mock): - mock.return_value = self._return_value( - {"next": [self.TEST_PHOTOS_DICT[0]], - "previous": [self.TEST_PHOTOS_DICT[1]]}) - result = self.TEST_PHOTOS[0].next_previous() - mock.assert_called_with("/photo/1a/nextprevious.json") + def test_photo_object_next_previous(self, mock_get): + """ + Check that the next/previous photos are returned + when using the photo object directly + """ + mock_get.return_value = self._return_value( + {"next": [self.test_photos_dict[0]], + "previous": [self.test_photos_dict[1]]}) + result = self.test_photos[0].next_previous() + mock_get.assert_called_with("/photo/1a/nextprevious.json") self.assertEqual(result["next"][0].get_fields(), - self.TEST_PHOTOS_DICT[0]) + self.test_photos_dict[0]) self.assertEqual(result["previous"][0].get_fields(), - self.TEST_PHOTOS_DICT[1]) + self.test_photos_dict[1]) @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photo_next(self, mock): - mock.return_value = self._return_value( - {"next": [self.TEST_PHOTOS_DICT[0]]}) - result = self.client.photo.next_previous(self.TEST_PHOTOS[0]) - mock.assert_called_with("/photo/1a/nextprevious.json") + def test_photo_next(self, mock_get): + """Check that the next photos are returned""" + mock_get.return_value = self._return_value( + {"next": [self.test_photos_dict[0]]}) + result = self.client.photo.next_previous(self.test_photos[0]) + mock_get.assert_called_with("/photo/1a/nextprevious.json") self.assertEqual(result["next"][0].get_fields(), - self.TEST_PHOTOS_DICT[0]) + self.test_photos_dict[0]) self.assertNotIn("previous", result) @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photo_previous(self, mock): - mock.return_value = self._return_value( - {"previous": [self.TEST_PHOTOS_DICT[1]]}) - result = self.client.photo.next_previous(self.TEST_PHOTOS[0]) - mock.assert_called_with("/photo/1a/nextprevious.json") + def test_photo_previous(self, mock_get): + """Check that the previous photos are returned""" + mock_get.return_value = self._return_value( + {"previous": [self.test_photos_dict[1]]}) + result = self.client.photo.next_previous(self.test_photos[0]) + mock_get.assert_called_with("/photo/1a/nextprevious.json") self.assertEqual(result["previous"][0].get_fields(), - self.TEST_PHOTOS_DICT[1]) + self.test_photos_dict[1]) self.assertNotIn("next", result) @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_photo_multiple_next_previous(self, mock): - mock.return_value = self._return_value( - {"next": [self.TEST_PHOTOS_DICT[0], self.TEST_PHOTOS_DICT[0]], - "previous": [self.TEST_PHOTOS_DICT[1], self.TEST_PHOTOS_DICT[1]]}) - result = self.client.photo.next_previous(self.TEST_PHOTOS[0]) - mock.assert_called_with("/photo/1a/nextprevious.json") + def test_photo_multiple_next_previous(self, mock_get): + """Check that multiple next/previous photos are returned""" + mock_get.return_value = self._return_value( + {"next": [self.test_photos_dict[0], self.test_photos_dict[0]], + "previous": [self.test_photos_dict[1], self.test_photos_dict[1]]}) + result = self.client.photo.next_previous(self.test_photos[0]) + mock_get.assert_called_with("/photo/1a/nextprevious.json") self.assertEqual(result["next"][0].get_fields(), - self.TEST_PHOTOS_DICT[0]) + self.test_photos_dict[0]) self.assertEqual(result["next"][1].get_fields(), - self.TEST_PHOTOS_DICT[0]) + self.test_photos_dict[0]) self.assertEqual(result["previous"][0].get_fields(), - self.TEST_PHOTOS_DICT[1]) + self.test_photos_dict[1]) self.assertEqual(result["previous"][1].get_fields(), - self.TEST_PHOTOS_DICT[1]) + self.test_photos_dict[1]) class TestPhotoTransform(TestPhotos): @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_transform(self, mock): - mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) - result = self.client.photo.transform(self.TEST_PHOTOS[0], rotate="90") - mock.assert_called_with("/photo/1a/transform.json", rotate="90") - self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[1]) + def test_photo_transform(self, mock_post): + """Check that a photo can be transformed""" + mock_post.return_value = self._return_value(self.test_photos_dict[1]) + result = self.client.photo.transform(self.test_photos[0], rotate="90") + mock_post.assert_called_with("/photo/1a/transform.json", rotate="90") + self.assertEqual(result.get_fields(), self.test_photos_dict[1]) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_transform_id(self, mock): - mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) + def test_photo_transform_id(self, mock_post): + """Check that a photo can be transformed using its ID""" + mock_post.return_value = self._return_value(self.test_photos_dict[1]) result = self.client.photo.transform("1a", rotate="90") - mock.assert_called_with("/photo/1a/transform.json", rotate="90") - self.assertEqual(result.get_fields(), self.TEST_PHOTOS_DICT[1]) + mock_post.assert_called_with("/photo/1a/transform.json", rotate="90") + self.assertEqual(result.get_fields(), self.test_photos_dict[1]) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_photo_object_transform(self, mock): - mock.return_value = self._return_value(self.TEST_PHOTOS_DICT[1]) - photo = self.TEST_PHOTOS[0] + def test_photo_object_transform(self, mock_post): + """ + Check that a photo can be transformed + when using the photo object directly + """ + mock_post.return_value = self._return_value(self.test_photos_dict[1]) + photo = self.test_photos[0] photo.transform(rotate="90") - mock.assert_called_with("/photo/1a/transform.json", rotate="90") - self.assertEqual(photo.get_fields(), self.TEST_PHOTOS_DICT[1]) + mock_post.assert_called_with("/photo/1a/transform.json", rotate="90") + self.assertEqual(photo.get_fields(), self.test_photos_dict[1]) diff --git a/tests/unit/test_tags.py b/tests/unit/test_tags.py index 562092c..76fac62 100644 --- a/tests/unit/test_tags.py +++ b/tests/unit/test_tags.py @@ -8,14 +8,15 @@ except ImportError: import openphoto class TestTags(unittest.TestCase): - TEST_HOST = "test.example.com" - TEST_TAGS_DICT = [{"count": 11, "id":"tag1"}, + test_host = "test.example.com" + test_tags = None + test_tags_dict = [{"count": 11, "id":"tag1"}, {"count": 5, "id":"tag2"}] def setUp(self): - self.client = openphoto.OpenPhoto(host=self.TEST_HOST) - self.TEST_TAGS = [openphoto.objects.Tag(self.client, tag) - for tag in self.TEST_TAGS_DICT] + self.client = openphoto.OpenPhoto(host=self.test_host) + self.test_tags = [openphoto.objects.Tag(self.client, tag) + for tag in self.test_tags_dict] @staticmethod def _return_value(result, message="", code=200): @@ -23,10 +24,11 @@ class TestTags(unittest.TestCase): class TestTagsList(TestTags): @mock.patch.object(openphoto.OpenPhoto, 'get') - def test_tags_list(self, mock): - mock.return_value = self._return_value(self.TEST_TAGS_DICT) + def test_tags_list(self, mock_get): + """Check that the the tag list is returned correctly""" + mock_get.return_value = self._return_value(self.test_tags_dict) result = self.client.tags.list() - mock.assert_called_with("/tags/list.json") + mock_get.assert_called_with("/tags/list.json") self.assertEqual(len(result), 2) self.assertEqual(result[0].id, "tag1") self.assertEqual(result[0].count, 11) @@ -37,43 +39,48 @@ class TestTagCreate(TestTags): # TODO: should return a tag object, not a result dict @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_tag_create(self, mock): - mock.return_value = self._return_value(self.TEST_TAGS_DICT[0]) + def test_tag_create(self, mock_post): + """Check that a tag can be created""" + mock_post.return_value = self._return_value(self.test_tags_dict[0]) result = self.client.tag.create(tag="Test", foo="bar") - mock.assert_called_with("/tag/create.json", tag="Test", foo="bar") + mock_post.assert_called_with("/tag/create.json", tag="Test", foo="bar") self.assertEqual(result.id, "tag1") self.assertEqual(result.count, 11) class TestTagDelete(TestTags): @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_tag_delete(self, mock): - mock.return_value = self._return_value(True) - result = self.client.tag.delete(self.TEST_TAGS[0]) - mock.assert_called_with("/tag/tag1/delete.json") + def test_tag_delete(self, mock_post): + """Check that a tag can be deleted""" + mock_post.return_value = self._return_value(True) + result = self.client.tag.delete(self.test_tags[0]) + mock_post.assert_called_with("/tag/tag1/delete.json") self.assertEqual(result, True) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_tag_delete_id(self, mock): - mock.return_value = self._return_value(True) + def test_tag_delete_id(self, mock_post): + """Check that a tag can be deleted using its ID""" + mock_post.return_value = self._return_value(True) result = self.client.tag.delete("tag1") - mock.assert_called_with("/tag/tag1/delete.json") + mock_post.assert_called_with("/tag/tag1/delete.json") self.assertEqual(result, True) # TODO: tag.delete should raise exception on failure @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_tag_delete_failure_raises_exception(self, mock): - mock.return_value = self._return_value(False) + def test_tag_delete_failure(self, mock_post): + """Check that an exception is raised if a tag cannot be deleted""" + mock_post.return_value = self._return_value(False) with self.assertRaises(openphoto.OpenPhotoError): - self.client.tag.delete(self.TEST_TAGS[0]) + self.client.tag.delete(self.test_tags[0]) # TODO: after deleting object fields, id should be set to None @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_tag_object_delete(self, mock): - mock.return_value = self._return_value(True) - tag = self.TEST_TAGS[0] + def test_tag_object_delete(self, mock_post): + """Check that a tag can be deleted when using the tag object directly""" + mock_post.return_value = self._return_value(True) + tag = self.test_tags[0] result = tag.delete() - mock.assert_called_with("/tag/tag1/delete.json") + mock_post.assert_called_with("/tag/tag1/delete.json") self.assertEqual(result, True) self.assertEqual(tag.get_fields(), {}) # self.assertEqual(tag.id, None) @@ -81,34 +88,41 @@ class TestTagDelete(TestTags): # TODO: tag.delete should raise exception on failure @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_tag_object_delete_failure_raises_exception(self, mock): - mock.return_value = self._return_value(False) + def test_tag_object_delete_failure(self, mock_post): + """ + Check that an exception is raised if a tag cannot be deleted + when using the tag object directly + """ + mock_post.return_value = self._return_value(False) with self.assertRaises(openphoto.OpenPhotoError): - self.TEST_TAGS[0].delete() + self.test_tags[0].delete() class TestTagUpdate(TestTags): @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_tag_update(self, mock): - mock.return_value = self._return_value(self.TEST_TAGS_DICT[1]) - result = self.client.tag.update(self.TEST_TAGS[0], name="Test") - mock.assert_called_with("/tag/tag1/update.json", name="Test") + def test_tag_update(self, mock_post): + """Check that a tag can be updated""" + mock_post.return_value = self._return_value(self.test_tags_dict[1]) + result = self.client.tag.update(self.test_tags[0], name="Test") + mock_post.assert_called_with("/tag/tag1/update.json", name="Test") self.assertEqual(result.id, "tag2") self.assertEqual(result.count, 5) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_tag_update_id(self, mock): - mock.return_value = self._return_value(self.TEST_TAGS_DICT[1]) + def test_tag_update_id(self, mock_post): + """Check that a tag can be updated using its ID""" + mock_post.return_value = self._return_value(self.test_tags_dict[1]) result = self.client.tag.update("tag1", name="Test") - mock.assert_called_with("/tag/tag1/update.json", name="Test") + mock_post.assert_called_with("/tag/tag1/update.json", name="Test") self.assertEqual(result.id, "tag2") self.assertEqual(result.count, 5) @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_tag_object_update(self, mock): - mock.return_value = self._return_value(self.TEST_TAGS_DICT[1]) - tag = self.TEST_TAGS[0] + def test_tag_object_update(self, mock_post): + """Check that a tag can be updated when using the tag object directly""" + mock_post.return_value = self._return_value(self.test_tags_dict[1]) + tag = self.test_tags[0] tag.update(name="Test") - mock.assert_called_with("/tag/tag1/update.json", name="Test") + mock_post.assert_called_with("/tag/tag1/update.json", name="Test") self.assertEqual(tag.id, "tag2") self.assertEqual(tag.count, 5) From 7359c1ae5aaa863aa145b1a422cd69a97aba51ed Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 20:11:09 +0100 Subject: [PATCH 18/58] Ensure files are closed properly, to avoid Python3 warning --- openphoto/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openphoto/main.py b/openphoto/main.py index 4849625..59a41a7 100644 --- a/openphoto/main.py +++ b/openphoto/main.py @@ -81,6 +81,8 @@ def main(args=sys.argv[1:]): params, files = extract_files(params) result = client.post(options.endpoint, process_response=False, files=files, **params) + for f in files: + files[f].close() if options.verbose: print("==========\nMethod: %s\nHost: %s\nEndpoint: %s" % From e662f32cc3290b7c58c11494ad542032a09d6cd0 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 20:23:31 +0100 Subject: [PATCH 19/58] Ensure the test file is closed after use to prevent Python3 warnings --- tests/unit/test_photos.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index 8e55664..bf783fe 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -299,11 +299,12 @@ class TestPhotoUpload(TestPhotos): @mock.patch.object(openphoto.OpenPhoto, 'post') def test_photo_upload_encoded(self, mock_post): """Check that a photo can be uploaded using Base64 encoding""" - encoded_file = base64.b64encode(open(self.test_file, "rb").read()) mock_post.return_value = self._return_value(self.test_photos_dict[0]) result = self.client.photo.upload_encoded(self.test_file, title="Test") - mock_post.assert_called_with("/photo/upload.json", - photo=encoded_file, title="Test") + with open(self.test_file, "rb") as in_file: + encoded_file = base64.b64encode(in_file.read()) + mock_post.assert_called_with("/photo/upload.json", + photo=encoded_file, title="Test") self.assertEqual(result.get_fields(), self.test_photos_dict[0]) class TestPhotoDynamicUrl(TestPhotos): From af4260937f8c42e11661e288c250a70eb0bbdb78 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 20:24:12 +0100 Subject: [PATCH 20/58] Unit test fixes for Python3 support --- tests/unit/test_cli.py | 28 ++++++++++++++++------------ tests/unit/test_http.py | 17 +++++++++-------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 07fce91..c33c47b 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -1,7 +1,11 @@ +from __future__ import unicode_literals import os import sys -from StringIO import StringIO import mock +try: + import StringIO as io # Python2 +except ImportError: + import io # Python3 try: import unittest2 as unittest # Python2.6 except ImportError: @@ -20,7 +24,7 @@ class TestCli(unittest.TestCase): test_file = os.path.join("tests", "unit", "data", "test_file.txt") @mock.patch.object(openphoto.main, "OpenPhoto") - @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('sys.stdout', new_callable=io.StringIO) def test_defaults(self, _, mock_openphoto): """Check that the default behaviour is correct""" get = mock_openphoto.return_value.get @@ -29,14 +33,14 @@ class TestCli(unittest.TestCase): get.assert_called_with("/photos/list.json", process_response=False) @mock.patch.object(openphoto.main, "OpenPhoto") - @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('sys.stdout', new_callable=io.StringIO) def test_config(self, _, mock_openphoto): """Check that a config file can be specified""" main(["--config=test"]) mock_openphoto.assert_called_with(config_file="test") @mock.patch.object(openphoto.main, "OpenPhoto") - @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('sys.stdout', new_callable=io.StringIO) def test_get(self, mock_stdout, mock_openphoto): """Check that the get operation is working""" get = mock_openphoto.return_value.get @@ -49,7 +53,7 @@ class TestCli(unittest.TestCase): self.assertEqual(mock_stdout.getvalue(), "Result\n") @mock.patch.object(openphoto.main, "OpenPhoto") - @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('sys.stdout', new_callable=io.StringIO) def test_post(self, mock_stdout, mock_openphoto): """Check that the post operation is working""" post = mock_openphoto.return_value.post @@ -62,7 +66,7 @@ class TestCli(unittest.TestCase): self.assertEqual(mock_stdout.getvalue(), "Result\n") @mock.patch.object(openphoto.main, "OpenPhoto") - @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('sys.stdout', new_callable=io.StringIO) def test_post_files(self, _, mock_openphoto): """Check that files are posted correctly""" post = mock_openphoto.return_value.post @@ -70,11 +74,11 @@ class TestCli(unittest.TestCase): # It's not possible to directly compare the file object, # so check it manually files = post.call_args[1]["files"] - self.assertEqual(files.keys(), ["photo"]) + self.assertEqual(list(files.keys()), ["photo"]) self.assertEqual(files["photo"].name, self.test_file) @mock.patch.object(sys, "exit", raise_exception) - @mock.patch('sys.stderr', new_callable=StringIO) + @mock.patch('sys.stderr', new_callable=io.StringIO) def test_unknown_arg(self, mock_stderr): """Check that an unknown argument produces an error""" with self.assertRaises(TestException): @@ -82,7 +86,7 @@ class TestCli(unittest.TestCase): self.assertIn("error: Unknown argument", mock_stderr.getvalue()) @mock.patch.object(sys, "exit", raise_exception) - @mock.patch('sys.stderr', new_callable=StringIO) + @mock.patch('sys.stderr', new_callable=io.StringIO) def test_unknown_option(self, mock_stderr): """Check that an unknown option produces an error""" with self.assertRaises(TestException): @@ -90,7 +94,7 @@ class TestCli(unittest.TestCase): self.assertIn("error: no such option", mock_stderr.getvalue()) @mock.patch.object(sys, "exit", raise_exception) - @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('sys.stdout', new_callable=io.StringIO) def test_unknown_config(self, mock_stdout): """Check that an unknown config file produces an error""" with self.assertRaises(TestException): @@ -101,7 +105,7 @@ class TestCli(unittest.TestCase): self.assertIn("To get your credentials", mock_stdout.getvalue()) @mock.patch.object(openphoto.main, "OpenPhoto") - @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('sys.stdout', new_callable=io.StringIO) def test_verbose(self, mock_stdout, _): """Check that the verbose option is working""" main(["-v"]) @@ -109,7 +113,7 @@ class TestCli(unittest.TestCase): self.assertIn("Endpoint: /photos/list.json", mock_stdout.getvalue()) @mock.patch.object(openphoto.main, "OpenPhoto") - @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('sys.stdout', new_callable=io.StringIO) def test_pretty_print(self, mock_stdout, mock_openphoto): """Check that the pretty-print option is working""" get = mock_openphoto.return_value.get diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py index ba092b7..899d24f 100644 --- a/tests/unit/test_http.py +++ b/tests/unit/test_http.py @@ -57,8 +57,8 @@ class TestHttp(unittest.TestCase): self.assertEqual(self._last_request().querystring["spam"], ["eggs"]) self.assertEqual(response, self.test_data) self.assertEqual(self.client.last_url, self.test_uri) - self.assertEqual(self.client.last_params, {"foo": "bar", - "spam": "eggs"}) + self.assertEqual(self.client.last_params, {"foo": b"bar", + "spam": b"eggs"}) self.assertEqual(self.client.last_response.json(), self.test_data) @httpretty.activate @@ -67,11 +67,12 @@ class TestHttp(unittest.TestCase): self._register_uri(httpretty.POST) response = self.client.post(self.test_endpoint, foo="bar", spam="eggs") - self.assertEqual(self._last_request().body, "foo=bar&spam=eggs") + self.assertIn(b"spam=eggs", self._last_request().body) + self.assertIn(b"foo=bar", self._last_request().body) self.assertEqual(response, self.test_data) self.assertEqual(self.client.last_url, self.test_uri) - self.assertEqual(self.client.last_params, {"foo": "bar", - "spam": "eggs"}) + self.assertEqual(self.client.last_params, {"foo": b"bar", + "spam": b"eggs"}) self.assertEqual(self.client.last_response.json(), self.test_data) @httpretty.activate @@ -123,7 +124,7 @@ class TestHttp(unittest.TestCase): self.assertEqual(params["tag"], ["tag_id"]) self.assertEqual(params["list_"], ["photo_id,album_id,tag_id"]) self.assertEqual(params["boolean"], ["1"]) - self.assertEqual(params["unicode_"], ["\xc3\xbcmlaut"]) + self.assertIn(params["unicode_"], [["\xc3\xbcmlaut"], ["\xfcmlaut"]]) @httpretty.activate def test_get_with_api_version(self): @@ -152,10 +153,10 @@ class TestHttp(unittest.TestCase): response = self.client.post(self.test_endpoint, files={"file": in_file}) self.assertEqual(response, self.test_data) - body = self._last_request().body + body = str(self._last_request().body) self.assertIn("Content-Disposition: form-data; "+ "name=\"file\"; filename=\"test_file.txt\"", body) - self.assertIn("Test File", body) + self.assertIn("Test File", str(body)) @httpretty.activate From 6387877e48407e91b73a2117f8e0f7cdaede4100 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 20:25:29 +0100 Subject: [PATCH 21/58] Monkey-patch httpretty for Python3 support This fix is in the httpretty codebase, but not yet released --- tests/unit/test_http_errors.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit/test_http_errors.py b/tests/unit/test_http_errors.py index ddd25ec..d95a838 100644 --- a/tests/unit/test_http_errors.py +++ b/tests/unit/test_http_errors.py @@ -1,6 +1,11 @@ from __future__ import unicode_literals import json import httpretty + +# TEMP: Temporary hack until httpretty string checking is fixed +if httpretty.compat.PY3: + httpretty.core.basestring = (bytes, str) + try: import unittest2 as unittest # Python2.6 except ImportError: From a1f22c0b6d71798cea4f6f56734c17b683a9c426 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 20:26:30 +0100 Subject: [PATCH 22/58] requests-oauthlib 0.3.2 introduced a Unicode bug. Hold back to 0.3.1 until this is resolved --- tox.ini | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 5e5c469..540d538 100644 --- a/tox.ini +++ b/tox.ini @@ -4,13 +4,14 @@ envlist = py26, py27, py33 [testenv] commands = python -m unittest discover --catch tests/unit deps = - mock - httpretty + requests_oauthlib == 0.3.1 # since 0.3.2 introduced a Unicode bug + mock >= 1.0.0 + httpretty >= 0.6.1 [testenv:py26] commands = unit2 discover --catch tests/unit deps = - mock - httpretty + mock >= 1.0.0 + httpretty >= 0.6.1 unittest2 discover From 2c6841bdd778187a695ba0fb376bda7d74991552 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 20:32:56 +0100 Subject: [PATCH 23/58] Travis now tests all supported Python versions --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ee74c8c..e8f456e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ install: - pip install tox --use-mirrors - .travis/install_pylint -script: tox -e py27 +script: tox after_script: # Run Pylint From 9cc884932226466251775c53c4c312a9a31ee88d Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 29 Jun 2013 21:24:33 +0100 Subject: [PATCH 24/58] Latest requests_oauthlib fixes a Unicode bug, so we don't need to ask requests to decode anymore --- openphoto/openphoto_http.py | 10 ++-------- tox.ini | 1 - 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/openphoto/openphoto_http.py b/openphoto/openphoto_http.py index 178057e..86a8b3d 100644 --- a/openphoto/openphoto_http.py +++ b/openphoto/openphoto_http.py @@ -14,12 +14,8 @@ from openphoto.config import Config if sys.version < '3': TEXT_TYPE = unicode - # requests_oauth needs to decode to ascii for Python2 - OAUTH_DECODING = "utf-8" else: TEXT_TYPE = str - # requests_oauth needs to use (unicode) strings for Python3 - OAUTH_DECODING = None DUPLICATE_RESPONSE = {"code": 409, "message": "This photo already exists"} @@ -76,8 +72,7 @@ class OpenPhotoHttp: auth = requests_oauthlib.OAuth1(self.config.consumer_key, self.config.consumer_secret, self.config.token, - self.config.token_secret, - decoding=OAUTH_DECODING) + self.config.token_secret) else: auth = None @@ -122,8 +117,7 @@ class OpenPhotoHttp: auth = requests_oauthlib.OAuth1(self.config.consumer_key, self.config.consumer_secret, self.config.token, - self.config.token_secret, - decoding=OAUTH_DECODING) + self.config.token_secret) with requests.Session() as session: if files: # Need to pass parameters as URL query, so they get OAuth signed diff --git a/tox.ini b/tox.ini index 540d538..2657d68 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,6 @@ envlist = py26, py27, py33 [testenv] commands = python -m unittest discover --catch tests/unit deps = - requests_oauthlib == 0.3.1 # since 0.3.2 introduced a Unicode bug mock >= 1.0.0 httpretty >= 0.6.1 From a54ec012549da4485e72e023ff0fc3a05313cacc Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 30 Jun 2013 17:05:50 +0100 Subject: [PATCH 25/58] Updated functional test instructions --- tests/functional/README.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/README.markdown b/tests/functional/README.markdown index 64601a8..9f4f804 100644 --- a/tests/functional/README.markdown +++ b/tests/functional/README.markdown @@ -38,14 +38,14 @@ The following instructions are for Python 2.7. You can adapt them for earlier Python versions using the ``unittest2`` package. cd /path/to/openphoto-python - python -m unittest discover -c + python -m unittest discover -c tests/functional The "-c" lets you stop the tests gracefully with \[CTRL\]-c. The easiest way to run a subset of the tests is with the ``nose`` package: cd /path/to/openphoto-python - nosetests -v -s --nologcapture tests/test_albums.py:TestAlbums.test_view + nosetests -v -s --nologcapture tests/functional/test_albums.py:TestAlbums.test_view All HTTP requests and responses are recorded in the file ``tests.log``. From 56d376d50e3d0ef04a2f6ba27350bf17e90e3482 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 30 Jun 2013 17:08:54 +0100 Subject: [PATCH 26/58] Added tox instructions --- tests/README.markdown | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/README.markdown b/tests/README.markdown index 320b5b3..793ae68 100644 --- a/tests/README.markdown +++ b/tests/README.markdown @@ -12,11 +12,16 @@ They run very quickly and don't require any external test hosts. #### Requirements * mock >= 1.0.0 * httpretty >= 0.6.1 + * tox (optional) #### Running the Unit Tests python -m unittest discover tests/unit +To run the unit tests against all supported Python versions, use ```tox```: + + tox + ---------------------------------------- ###Functional Tests From 921410d8f8898ffb42ab313a9f3b8896ff94741e Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 30 Jun 2013 17:17:03 +0100 Subject: [PATCH 27/58] 404 status should raise 404 error, even if JSON is valid. --- openphoto/openphoto_http.py | 6 +++--- tests/unit/test_http_errors.py | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/openphoto/openphoto_http.py b/openphoto/openphoto_http.py index 86a8b3d..d1e2475 100644 --- a/openphoto/openphoto_http.py +++ b/openphoto/openphoto_http.py @@ -183,6 +183,9 @@ class OpenPhotoHttp: Decodes the JSON response, returning a dict. Raises an exception if an invalid response code is received. """ + if response.status_code == 404: + raise OpenPhoto404Error("HTTP Error %d: %s" % + (response.status_code, response.reason)) try: json_response = response.json() code = json_response["code"] @@ -192,9 +195,6 @@ class OpenPhotoHttp: if 200 <= response.status_code < 300: # Status code was valid, so just reraise the exception raise - elif response.status_code == 404: - raise OpenPhoto404Error("HTTP Error %d: %s" % - (response.status_code, response.reason)) else: raise OpenPhotoError("HTTP Error %d: %s" % (response.status_code, response.reason)) diff --git a/tests/unit/test_http_errors.py b/tests/unit/test_http_errors.py index d95a838..56b1d19 100644 --- a/tests/unit/test_http_errors.py +++ b/tests/unit/test_http_errors.py @@ -61,8 +61,6 @@ class TestHttpErrors(unittest.TestCase): with self.assertRaises(openphoto.OpenPhotoError): self.client.post(self.test_endpoint) - # TODO: 404 status should raise 404 error, even if JSON is valid - @unittest.expectedFailure @httpretty.activate def test_get_with_404_status(self): """ @@ -73,8 +71,6 @@ class TestHttpErrors(unittest.TestCase): with self.assertRaises(openphoto.OpenPhoto404Error): self.client.get(self.test_endpoint) - # TODO: 404 status should raise 404 error, even if JSON is valid - @unittest.expectedFailure @httpretty.activate def test_post_with_404_status(self): """ From 280521cf4a89a928aa60209e88f551d267c5de9d Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 30 Jun 2013 17:21:20 +0100 Subject: [PATCH 28/58] Mismatch between response status and JSON code raises an exception --- openphoto/openphoto_http.py | 4 ++++ tests/unit/test_http_errors.py | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openphoto/openphoto_http.py b/openphoto/openphoto_http.py index d1e2475..7e0ec4f 100644 --- a/openphoto/openphoto_http.py +++ b/openphoto/openphoto_http.py @@ -199,6 +199,10 @@ class OpenPhotoHttp: raise OpenPhotoError("HTTP Error %d: %s" % (response.status_code, response.reason)) + if code != response.status_code: + raise OpenPhotoError(("Response status code %d does not match " + + "JSON status code %d") % (response.status_code, + code)) if 200 <= code < 300: return json_response elif (code == DUPLICATE_RESPONSE["code"] and diff --git a/tests/unit/test_http_errors.py b/tests/unit/test_http_errors.py index 56b1d19..90373a7 100644 --- a/tests/unit/test_http_errors.py +++ b/tests/unit/test_http_errors.py @@ -163,8 +163,6 @@ class TestHttpErrors(unittest.TestCase): with self.assertRaises(openphoto.OpenPhotoDuplicateError): self.client.post(self.test_endpoint) - # TODO: Status code mismatch should raise an exception - @unittest.expectedFailure @httpretty.activate def test_get_with_status_code_mismatch(self): """ @@ -176,8 +174,6 @@ class TestHttpErrors(unittest.TestCase): with self.assertRaises(openphoto.OpenPhotoError): self.client.get(self.test_endpoint) - # TODO: Status code mismatch should raise an exception - @unittest.expectedFailure @httpretty.activate def test_post_with_status_code_mismatch(self): """ From 3f1757d0aa8b3d1fc61d20596218625560ffd24d Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 30 Jun 2013 17:30:25 +0100 Subject: [PATCH 29/58] photos.update/update now also accept a list of Photo objects --- openphoto/api_photo.py | 19 +++++++++++++++++-- tests/unit/test_photos.py | 4 ---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/openphoto/api_photo.py b/openphoto/api_photo.py index cfa2c45..c5052ef 100644 --- a/openphoto/api_photo.py +++ b/openphoto/api_photo.py @@ -4,6 +4,19 @@ from openphoto.errors import OpenPhotoError import openphoto.openphoto_http from openphoto.objects import Photo +def extract_ids(photos): + """ + Given a list of objects, extract the photo id for each Photo + object. + """ + ids = [] + for photo in photos: + if isinstance(photo, Photo): + ids.append(photo.id) + else: + ids.append(photo) + return ids + class ApiPhotos: def __init__(self, client): self._client = client @@ -20,7 +33,8 @@ class ApiPhotos: Returns True if successful. Raises OpenPhotoError if not. """ - if not self._client.post("/photos/update.json", ids=photos, + ids = extract_ids(photos) + if not self._client.post("/photos/update.json", ids=ids, **kwds)["result"]: raise OpenPhotoError("Update response returned False") return True @@ -31,7 +45,8 @@ class ApiPhotos: Returns True if successful. Raises OpenPhotoError if not. """ - if not self._client.post("/photos/delete.json", ids=photos, + ids = extract_ids(photos) + if not self._client.post("/photos/delete.json", ids=ids, **kwds)["result"]: raise OpenPhotoError("Delete response returned False") return True diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index bf783fe..6a53677 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -40,8 +40,6 @@ class TestPhotosList(TestPhotos): self.assertEqual(result[1].tags, ["tag3", "tag4"]) class TestPhotosUpdate(TestPhotos): - # TODO: photos.update should accept a list of Photo objects - @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') def test_photos_update(self, mock_post): """Check that multiple photos can be updated""" @@ -71,8 +69,6 @@ class TestPhotosUpdate(TestPhotos): self.client.photos.update(self.test_photos, title="Test") class TestPhotosDelete(TestPhotos): - # TODO: photos.delete should accept a list of Photo objects - @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') def test_photos_delete(self, mock_post): """Check that multiple photos can be deleted""" From 9968d7f265170fca53bf800de1dbe62a57d0b999 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 30 Jun 2013 17:34:41 +0100 Subject: [PATCH 30/58] Delete methods should raise exception on failure, rather than returning False --- openphoto/objects.py | 8 ++++++++ tests/unit/test_albums.py | 4 ---- tests/unit/test_photos.py | 4 ---- tests/unit/test_tags.py | 4 ---- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/openphoto/objects.py b/openphoto/objects.py index 6d5daa2..61b4b7c 100644 --- a/openphoto/objects.py +++ b/openphoto/objects.py @@ -3,6 +3,8 @@ try: except ImportError: from urllib import quote # Python2 +from openphoto.errors import OpenPhotoError + class OpenPhotoObject: """ Base object supporting the storage of custom fields as attributes """ def __init__(self, openphoto, json_dict): @@ -51,6 +53,8 @@ class Photo(OpenPhotoObject): """ result = self._openphoto.post("/photo/%s/delete.json" % self.id, **kwds)["result"] + if not result: + raise OpenPhotoError("Delete response returned False") self._replace_fields({}) return result @@ -136,6 +140,8 @@ class Tag(OpenPhotoObject): """ result = self._openphoto.post("/tag/%s/delete.json" % quote(self.id), **kwds)["result"] + if not result: + raise OpenPhotoError("Delete response returned False") self._replace_fields({}) return result @@ -172,6 +178,8 @@ class Album(OpenPhotoObject): """ result = self._openphoto.post("/album/%s/delete.json" % self.id, **kwds)["result"] + if not result: + raise OpenPhotoError("Delete response returned False") self._replace_fields({}) return result diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py index a117bd5..a29fd15 100644 --- a/tests/unit/test_albums.py +++ b/tests/unit/test_albums.py @@ -88,8 +88,6 @@ class TestAlbumDelete(TestAlbums): mock_post.assert_called_with("/album/1/delete.json") self.assertEqual(result, True) - # TODO: album.delete should raise exception on failure - @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') def test_album_delete_failure(self, mock_post): """Check that an exception is raised if an album cannot be deleted""" @@ -110,8 +108,6 @@ class TestAlbumDelete(TestAlbums): # self.assertEqual(album.id, None) # self.assertEqual(album.name, None) - # TODO: album.delete should raise exception on failure - @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') def test_album_object_delete_failure(self, mock_post): """ diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index 6a53677..92440aa 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -112,8 +112,6 @@ class TestPhotoDelete(TestPhotos): mock_post.assert_called_with("/photo/1a/delete.json") self.assertEqual(result, True) - # TODO: photo.delete should raise exception on failure - @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') def test_photo_delete_failure(self, mock_post): """Check that an exception is raised if a photo cannot be deleted""" @@ -136,8 +134,6 @@ class TestPhotoDelete(TestPhotos): self.assertEqual(photo.get_fields(), {}) # self.assertEqual(photo.id, None) - # TODO: photo.delete should raise exception on failure - @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') def test_photo_object_delete_failure(self, mock_post): """ diff --git a/tests/unit/test_tags.py b/tests/unit/test_tags.py index 76fac62..b266fbe 100644 --- a/tests/unit/test_tags.py +++ b/tests/unit/test_tags.py @@ -64,8 +64,6 @@ class TestTagDelete(TestTags): mock_post.assert_called_with("/tag/tag1/delete.json") self.assertEqual(result, True) - # TODO: tag.delete should raise exception on failure - @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') def test_tag_delete_failure(self, mock_post): """Check that an exception is raised if a tag cannot be deleted""" @@ -85,8 +83,6 @@ class TestTagDelete(TestTags): self.assertEqual(tag.get_fields(), {}) # self.assertEqual(tag.id, None) - # TODO: tag.delete should raise exception on failure - @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') def test_tag_object_delete_failure(self, mock_post): """ From 5656221147db05ae561e7b95c319eb49e08699f6 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 30 Jun 2013 17:39:02 +0100 Subject: [PATCH 31/58] After deleting an object's fields, name and id should be set to None --- openphoto/objects.py | 16 +++++++++++++--- tests/unit/test_albums.py | 5 ++--- tests/unit/test_photos.py | 3 +-- tests/unit/test_tags.py | 3 +-- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/openphoto/objects.py b/openphoto/objects.py index 61b4b7c..d95b803 100644 --- a/openphoto/objects.py +++ b/openphoto/objects.py @@ -31,6 +31,16 @@ class OpenPhotoObject: self._json_dict = json_dict self._set_fields(json_dict) + def _delete_fields(self): + """ + Delete this object's attributes, including name and id + """ + for key in self._json_dict.keys(): + delattr(self, key) + self._json_dict = {} + self.id = None + self.name = None + def __repr__(self): if self.name is not None: return "<%s name='%s'>" % (self.__class__, self.name) @@ -55,7 +65,7 @@ class Photo(OpenPhotoObject): self.id, **kwds)["result"] if not result: raise OpenPhotoError("Delete response returned False") - self._replace_fields({}) + self._delete_fields() return result def edit(self, **kwds): @@ -142,7 +152,7 @@ class Tag(OpenPhotoObject): quote(self.id), **kwds)["result"] if not result: raise OpenPhotoError("Delete response returned False") - self._replace_fields({}) + self._delete_fields() return result def update(self, **kwds): @@ -180,7 +190,7 @@ class Album(OpenPhotoObject): self.id, **kwds)["result"] if not result: raise OpenPhotoError("Delete response returned False") - self._replace_fields({}) + self._delete_fields() return result def form(self, **kwds): diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py index a29fd15..5751688 100644 --- a/tests/unit/test_albums.py +++ b/tests/unit/test_albums.py @@ -95,7 +95,6 @@ class TestAlbumDelete(TestAlbums): with self.assertRaises(openphoto.OpenPhotoError): self.client.album.delete(self.test_albums[0]) - # TODO: after deleting object fields, name and id should be set to None @mock.patch.object(openphoto.OpenPhoto, 'post') def test_album_object_delete(self, mock_post): """Check that an album can be deleted using the album object directly""" @@ -105,8 +104,8 @@ class TestAlbumDelete(TestAlbums): mock_post.assert_called_with("/album/1/delete.json") self.assertEqual(result, True) self.assertEqual(album.get_fields(), {}) - # self.assertEqual(album.id, None) - # self.assertEqual(album.name, None) + self.assertEqual(album.id, None) + self.assertEqual(album.name, None) @mock.patch.object(openphoto.OpenPhoto, 'post') def test_album_object_delete_failure(self, mock_post): diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index 92440aa..6cc43fa 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -119,7 +119,6 @@ class TestPhotoDelete(TestPhotos): with self.assertRaises(openphoto.OpenPhotoError): self.client.photo.delete(self.test_photos[0]) - # TODO: after deleting object fields, name and id should be set to None @mock.patch.object(openphoto.OpenPhoto, 'post') def test_photo_object_delete(self, mock_post): """ @@ -132,7 +131,7 @@ class TestPhotoDelete(TestPhotos): mock_post.assert_called_with("/photo/1a/delete.json") self.assertEqual(result, True) self.assertEqual(photo.get_fields(), {}) - # self.assertEqual(photo.id, None) + self.assertEqual(photo.id, None) @mock.patch.object(openphoto.OpenPhoto, 'post') def test_photo_object_delete_failure(self, mock_post): diff --git a/tests/unit/test_tags.py b/tests/unit/test_tags.py index b266fbe..c4ecd4a 100644 --- a/tests/unit/test_tags.py +++ b/tests/unit/test_tags.py @@ -71,7 +71,6 @@ class TestTagDelete(TestTags): with self.assertRaises(openphoto.OpenPhotoError): self.client.tag.delete(self.test_tags[0]) - # TODO: after deleting object fields, id should be set to None @mock.patch.object(openphoto.OpenPhoto, 'post') def test_tag_object_delete(self, mock_post): """Check that a tag can be deleted when using the tag object directly""" @@ -81,7 +80,7 @@ class TestTagDelete(TestTags): mock_post.assert_called_with("/tag/tag1/delete.json") self.assertEqual(result, True) self.assertEqual(tag.get_fields(), {}) - # self.assertEqual(tag.id, None) + self.assertEqual(tag.id, None) @mock.patch.object(openphoto.OpenPhoto, 'post') def test_tag_object_delete_failure(self, mock_post): From 413cf297a3a5a62d787e09926335aecad0733d34 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 30 Jun 2013 17:40:25 +0100 Subject: [PATCH 32/58] Replace_encoded parameter should be called photo_file, not encoded_photo --- openphoto/objects.py | 2 +- tests/unit/test_photos.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/openphoto/objects.py b/openphoto/objects.py index d95b803..8e40cc7 100644 --- a/openphoto/objects.py +++ b/openphoto/objects.py @@ -77,7 +77,7 @@ class Photo(OpenPhotoObject): def replace(self, photo_file, **kwds): raise NotImplementedError() - def replace_encoded(self, encoded_photo, **kwds): + def replace_encoded(self, photo_file, **kwds): raise NotImplementedError() def update(self, **kwds): diff --git a/tests/unit/test_photos.py b/tests/unit/test_photos.py index 6cc43fa..c76f4cc 100644 --- a/tests/unit/test_photos.py +++ b/tests/unit/test_photos.py @@ -203,9 +203,6 @@ class TestPhotoReplace(TestPhotos): with self.assertRaises(NotImplementedError): self.client.photo.replace_encoded("1a", self.test_file) - # TODO: replace_encoded parameter should be called photo_file, - # not encoded_photo - @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') def test_photo_object_replace_encoded(self, _): """ If photo.replace_encoded gets implemented, write a test! """ From 971bab4f7b62747a9e20e6de2d482b145704797b Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 30 Jun 2013 18:04:36 +0100 Subject: [PATCH 33/58] Status code mismatches are actually quite common. Rather than raise an exception, just ensure we return the JSON code. --- openphoto/openphoto_http.py | 4 ---- tests/unit/test_http_errors.py | 22 ++++++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/openphoto/openphoto_http.py b/openphoto/openphoto_http.py index 7e0ec4f..d1e2475 100644 --- a/openphoto/openphoto_http.py +++ b/openphoto/openphoto_http.py @@ -199,10 +199,6 @@ class OpenPhotoHttp: raise OpenPhotoError("HTTP Error %d: %s" % (response.status_code, response.reason)) - if code != response.status_code: - raise OpenPhotoError(("Response status code %d does not match " + - "JSON status code %d") % (response.status_code, - code)) if 200 <= code < 300: return json_response elif (code == DUPLICATE_RESPONSE["code"] and diff --git a/tests/unit/test_http_errors.py b/tests/unit/test_http_errors.py index 90373a7..d5c5988 100644 --- a/tests/unit/test_http_errors.py +++ b/tests/unit/test_http_errors.py @@ -166,22 +166,24 @@ class TestHttpErrors(unittest.TestCase): @httpretty.activate def test_get_with_status_code_mismatch(self): """ - Check that an exception is raised if a get returns a - status code that doesn't match the JSON code + Check that a mismatched HTTP status code still returns the + JSON status code for get requests. """ - data = {"message": "Test Message", "code": 200} - self._register_uri(httpretty.GET, data=data, status=202) - with self.assertRaises(openphoto.OpenPhotoError): - self.client.get(self.test_endpoint) + data = {"message": "Test Message", "code": 202} + self._register_uri(httpretty.GET, data=data, status=200) + response = self.client.get(self.test_endpoint) + self.assertEqual(response["code"], 202) + # TODO: Status code mismatch should raise an exception + @unittest.expectedFailure @httpretty.activate def test_post_with_status_code_mismatch(self): """ - Check that an exception is raised if a post returns a - status code that doesn't match the JSON code + Check that a mismatched HTTP status code still returns the + JSON status code for post requests. """ data = {"message": "Test Message", "code": 200} self._register_uri(httpretty.POST, data=data, status=202) - with self.assertRaises(openphoto.OpenPhotoError): - self.client.post(self.test_endpoint) + response = self.client.post(self.test_endpoint) + self.assertEqual(response["code"], 202) From 4c6b41dbc3e722ade21adb289b148874582b0b26 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 30 Jun 2013 18:10:15 +0100 Subject: [PATCH 34/58] Don't test the tag/create endpoint, as it is redundant and will be removed. See #38 for details. --- tests/unit/test_tags.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/unit/test_tags.py b/tests/unit/test_tags.py index c4ecd4a..f915b99 100644 --- a/tests/unit/test_tags.py +++ b/tests/unit/test_tags.py @@ -35,18 +35,6 @@ class TestTagsList(TestTags): self.assertEqual(result[1].id, "tag2") self.assertEqual(result[1].count, 5) -class TestTagCreate(TestTags): - # TODO: should return a tag object, not a result dict - @unittest.expectedFailure - @mock.patch.object(openphoto.OpenPhoto, 'post') - def test_tag_create(self, mock_post): - """Check that a tag can be created""" - mock_post.return_value = self._return_value(self.test_tags_dict[0]) - result = self.client.tag.create(tag="Test", foo="bar") - mock_post.assert_called_with("/tag/create.json", tag="Test", foo="bar") - self.assertEqual(result.id, "tag1") - self.assertEqual(result.count, 11) - class TestTagDelete(TestTags): @mock.patch.object(openphoto.OpenPhoto, 'post') def test_tag_delete(self, mock_post): From 33c700a3cdb53499b8558b0bb04b710bac441594 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 30 Jun 2013 18:15:28 +0100 Subject: [PATCH 35/58] Ensure album cover is updated to a Photo object during Album() initialisation --- openphoto/objects.py | 2 +- tests/unit/test_albums.py | 35 ++++++++++++++--------------------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/openphoto/objects.py b/openphoto/objects.py index 8e40cc7..4d1babb 100644 --- a/openphoto/objects.py +++ b/openphoto/objects.py @@ -164,9 +164,9 @@ class Tag(OpenPhotoObject): class Album(OpenPhotoObject): def __init__(self, openphoto, json_dict): - OpenPhotoObject.__init__(self, openphoto, json_dict) self.photos = None self.cover = None + OpenPhotoObject.__init__(self, openphoto, json_dict) self._update_fields_with_objects() def _update_fields_with_objects(self): diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py index 5751688..0b1b5f4 100644 --- a/tests/unit/test_albums.py +++ b/tests/unit/test_albums.py @@ -58,7 +58,6 @@ class TestAlbumsList(TestAlbums): self.assertEqual(result[1].cover.tags, ["tag3", "tag4"]) class TestAlbumCreate(TestAlbums): - # TODO: cover should be updated to Photo object @mock.patch.object(openphoto.OpenPhoto, 'post') def test_album_create(self, mock_post): """Check that an album can be created""" @@ -68,8 +67,8 @@ class TestAlbumCreate(TestAlbums): foo="bar") self.assertEqual(result.id, "1") self.assertEqual(result.name, "Album 1") - # self.assertEqual(result.cover.id, "1a") - # self.assertEqual(result.cover.tags, ["tag1", "tag2"]) + self.assertEqual(result.cover.id, "1a") + self.assertEqual(result.cover.tags, ["tag1", "tag2"]) class TestAlbumDelete(TestAlbums): @mock.patch.object(openphoto.OpenPhoto, 'post') @@ -180,7 +179,6 @@ class TestAlbumRemovePhotos(TestAlbums): self.test_albums[0].remove_photos(["Photo Objects"]) class TestAlbumUpdate(TestAlbums): - # TODO: cover should be updated to Photo object @mock.patch.object(openphoto.OpenPhoto, 'post') def test_album_update(self, mock_post): """Check that an album can be updated""" @@ -189,10 +187,9 @@ class TestAlbumUpdate(TestAlbums): mock_post.assert_called_with("/album/1/update.json", name="Test") self.assertEqual(result.id, "2") self.assertEqual(result.name, "Album 2") - # self.assertEqual(result.cover.id, "2b") - # self.assertEqual(result.cover.tags, ["tag3", "tag4"]) + self.assertEqual(result.cover.id, "2b") + self.assertEqual(result.cover.tags, ["tag3", "tag4"]) - # TODO: cover should be updated to Photo object @mock.patch.object(openphoto.OpenPhoto, 'post') def test_album_update_id(self, mock_post): """Check that an album can be updated using its ID""" @@ -201,10 +198,9 @@ class TestAlbumUpdate(TestAlbums): mock_post.assert_called_with("/album/1/update.json", name="Test") self.assertEqual(result.id, "2") self.assertEqual(result.name, "Album 2") - # self.assertEqual(result.cover.id, "2b") - # self.assertEqual(result.cover.tags, ["tag3", "tag4"]) + self.assertEqual(result.cover.id, "2b") + self.assertEqual(result.cover.tags, ["tag3", "tag4"]) - # TODO: cover should be updated to Photo object @mock.patch.object(openphoto.OpenPhoto, 'post') def test_album_object_update(self, mock_post): """Check that an album can be updated using the album object directly""" @@ -214,11 +210,10 @@ class TestAlbumUpdate(TestAlbums): mock_post.assert_called_with("/album/1/update.json", name="Test") self.assertEqual(album.id, "2") self.assertEqual(album.name, "Album 2") - # self.assertEqual(album.cover.id, "2b") - # self.assertEqual(album.cover.tags, ["tag3", "tag4"]) + self.assertEqual(album.cover.id, "2b") + self.assertEqual(album.cover.tags, ["tag3", "tag4"]) class TestAlbumView(TestAlbums): - # TODO: cover should be updated to Photo object @mock.patch.object(openphoto.OpenPhoto, 'get') def test_album_view(self, mock_get): """Check that an album can be viewed""" @@ -227,10 +222,9 @@ class TestAlbumView(TestAlbums): mock_get.assert_called_with("/album/1/view.json", name="Test") self.assertEqual(result.id, "2") self.assertEqual(result.name, "Album 2") - # self.assertEqual(result.cover.id, "2b") - # self.assertEqual(result.cover.tags, ["tag3", "tag4"]) + self.assertEqual(result.cover.id, "2b") + self.assertEqual(result.cover.tags, ["tag3", "tag4"]) - # TODO: cover should be updated to Photo object @mock.patch.object(openphoto.OpenPhoto, 'get') def test_album_view_id(self, mock_get): """Check that an album can be viewed using its ID""" @@ -239,10 +233,9 @@ class TestAlbumView(TestAlbums): mock_get.assert_called_with("/album/1/view.json", name="Test") self.assertEqual(result.id, "2") self.assertEqual(result.name, "Album 2") - # self.assertEqual(result.cover.id, "2b") - # self.assertEqual(result.cover.tags, ["tag3", "tag4"]) + self.assertEqual(result.cover.id, "2b") + self.assertEqual(result.cover.tags, ["tag3", "tag4"]) - # TODO: cover should be updated to Photo object @mock.patch.object(openphoto.OpenPhoto, 'get') def test_album_object_view(self, mock_get): """Check that an album can be viewed using the album object directly""" @@ -252,6 +245,6 @@ class TestAlbumView(TestAlbums): mock_get.assert_called_with("/album/1/view.json", name="Test") self.assertEqual(album.id, "2") self.assertEqual(album.name, "Album 2") - # self.assertEqual(album.cover.id, "2b") - # self.assertEqual(album.cover.tags, ["tag3", "tag4"]) + self.assertEqual(album.cover.id, "2b") + self.assertEqual(album.cover.tags, ["tag3", "tag4"]) From a23dbb36c5da252f93f6a5de08021fcdbc11f168 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 30 Jun 2013 18:16:09 +0100 Subject: [PATCH 36/58] Remove invalid comment --- tests/unit/test_albums.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py index 0b1b5f4..a2fb844 100644 --- a/tests/unit/test_albums.py +++ b/tests/unit/test_albums.py @@ -39,7 +39,6 @@ class TestAlbumsList(TestAlbums): self.assertEqual(result[1].id, "2") self.assertEqual(result[1].name, "Album 2") - # TODO: cover should be updated to Photo object @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'get') def test_albums_list_returns_cover_photos(self, mock_get): From 639836c2489efc35c719be264f7b008f0af32d00 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 30 Jun 2013 18:17:48 +0100 Subject: [PATCH 37/58] object.add/remove_photos should accept photos list as first parameter (even though it's not implemented yet) --- openphoto/objects.py | 4 ++-- tests/unit/test_albums.py | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/openphoto/objects.py b/openphoto/objects.py index 4d1babb..8072188 100644 --- a/openphoto/objects.py +++ b/openphoto/objects.py @@ -196,10 +196,10 @@ class Album(OpenPhotoObject): def form(self, **kwds): raise NotImplementedError() - def add_photos(self, **kwds): + def add_photos(self, photos, **kwds): raise NotImplementedError() - def remove_photos(self, **kwds): + def remove_photos(self, photos, **kwds): raise NotImplementedError() def update(self, **kwds): diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py index a2fb844..88054f1 100644 --- a/tests/unit/test_albums.py +++ b/tests/unit/test_albums.py @@ -147,8 +147,6 @@ class TestAlbumAddPhotos(TestAlbums): with self.assertRaises(NotImplementedError): self.client.album.add_photos("1", ["Photo Objects"]) - # TODO: object.add_photos should accept photos list as first parameter - @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') def test_album_object_add_photos(self, _): """ If album.add_photos gets implemented, write a test! """ @@ -169,8 +167,6 @@ class TestAlbumRemovePhotos(TestAlbums): with self.assertRaises(NotImplementedError): self.client.album.remove_photos("1", ["Photo Objects"]) - # TODO: object.remove_photos should accept photos list as first parameter - @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'post') def test_album_object_remove_photos(self, _): """ If album.remove_photos gets implemented, write a test! """ From b4861e4332b77c2a2961c5b9e4f6b659e6fecf0c Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 30 Jun 2013 18:25:05 +0100 Subject: [PATCH 38/58] Fix a couple of unit tests --- tests/unit/test_albums.py | 3 +-- tests/unit/test_http_errors.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_albums.py b/tests/unit/test_albums.py index 88054f1..3a5a5b2 100644 --- a/tests/unit/test_albums.py +++ b/tests/unit/test_albums.py @@ -39,7 +39,6 @@ class TestAlbumsList(TestAlbums): self.assertEqual(result[1].id, "2") self.assertEqual(result[1].name, "Album 2") - @unittest.expectedFailure @mock.patch.object(openphoto.OpenPhoto, 'get') def test_albums_list_returns_cover_photos(self, mock_get): """Check that the album list returns cover photo objects""" @@ -52,7 +51,7 @@ class TestAlbumsList(TestAlbums): self.assertEqual(result[0].cover.id, "1a") self.assertEqual(result[0].cover.tags, ["tag1", "tag2"]) self.assertEqual(result[1].id, "2") - self.assertEqual(result[0].name, "Album 2") + self.assertEqual(result[1].name, "Album 2") self.assertEqual(result[1].cover.id, "2b") self.assertEqual(result[1].cover.tags, ["tag3", "tag4"]) diff --git a/tests/unit/test_http_errors.py b/tests/unit/test_http_errors.py index d5c5988..557f7ca 100644 --- a/tests/unit/test_http_errors.py +++ b/tests/unit/test_http_errors.py @@ -174,16 +174,14 @@ class TestHttpErrors(unittest.TestCase): response = self.client.get(self.test_endpoint) self.assertEqual(response["code"], 202) - # TODO: Status code mismatch should raise an exception - @unittest.expectedFailure @httpretty.activate def test_post_with_status_code_mismatch(self): """ Check that a mismatched HTTP status code still returns the JSON status code for post requests. """ - data = {"message": "Test Message", "code": 200} - self._register_uri(httpretty.POST, data=data, status=202) + data = {"message": "Test Message", "code": 202} + self._register_uri(httpretty.POST, data=data, status=200) response = self.client.post(self.test_endpoint) self.assertEqual(response["code"], 202) From a250a87df41ab3ed828b1800939f8533453fb31f Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 6 Jul 2013 11:02:39 +0100 Subject: [PATCH 39/58] Move openphoto script to bin directory (as recommended in http://guide.python-distribute.org/) --- {scripts => bin}/openphoto | 0 setup.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {scripts => bin}/openphoto (100%) mode change 100644 => 100755 diff --git a/scripts/openphoto b/bin/openphoto old mode 100644 new mode 100755 similarity index 100% rename from scripts/openphoto rename to bin/openphoto diff --git a/setup.py b/setup.py index 74ecf8a..5d2c08a 100755 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ try: } except ImportError: from distutils.core import setup - kw = {'scripts': ['scripts/openphoto'], + kw = {'scripts': ['bin/openphoto'], 'requires': requires} setup(name='openphoto', From ded85d8273b34541371de28ac8cfbfdd2c4d589a Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 6 Jul 2013 11:08:07 +0100 Subject: [PATCH 40/58] Update metadata --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 5d2c08a..656f809 100755 --- a/setup.py +++ b/setup.py @@ -16,9 +16,8 @@ except ImportError: setup(name='openphoto', version='0.3', - description='Client library for the openphoto project', - author='James Walker', - author_email='walkah@walkah.net', + description='Python client library for Trovebox/Openphoto', + author='Pete Burgers, James Walker', url='https://github.com/openphoto/openphoto-python', packages=['openphoto'], **kw From e8e65f082ba73277cd99a02ede4d3f019a585088 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 6 Jul 2013 11:14:20 +0100 Subject: [PATCH 41/58] Correct requests_oauth package name, to fix distutils installation --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 656f809..309ed87 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -requires = ['requests', 'requests-oauthlib'] +requires = ['requests', 'requests_oauthlib'] try: from setuptools import setup From 9fc9177742e8f1c1ee4f35e796d05575c0f58108 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 6 Jul 2013 11:29:00 +0100 Subject: [PATCH 42/58] Store the openphoto-python version inside the package --- openphoto/__init__.py | 2 ++ setup.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/openphoto/__init__.py b/openphoto/__init__.py index b2523cf..07b07a1 100644 --- a/openphoto/__init__.py +++ b/openphoto/__init__.py @@ -4,6 +4,8 @@ import openphoto.api_photo import openphoto.api_tag import openphoto.api_album +__version__ = "0.3" + LATEST_API_VERSION = 2 class OpenPhoto(OpenPhotoHttp): diff --git a/setup.py b/setup.py index 309ed87..b978a24 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +import openphoto requires = ['requests', 'requests_oauthlib'] @@ -15,7 +16,7 @@ except ImportError: 'requires': requires} setup(name='openphoto', - version='0.3', + version=openphoto.__version__, description='Python client library for Trovebox/Openphoto', author='Pete Burgers, James Walker', url='https://github.com/openphoto/openphoto-python', From a8bde4a4383d79e118c4f3671b61e4bee3487145 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 6 Jul 2013 11:38:32 +0100 Subject: [PATCH 43/58] Add version option to CLI. --- openphoto/main.py | 12 +++++++++--- tests/unit/test_cli.py | 21 ++++++++++++++------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/openphoto/main.py b/openphoto/main.py index 59a41a7..4aacbc0 100644 --- a/openphoto/main.py +++ b/openphoto/main.py @@ -4,7 +4,7 @@ import sys import json from optparse import OptionParser -from openphoto import OpenPhoto +import openphoto CONFIG_ERROR = """ You must create a configuration file with the following contents: @@ -44,6 +44,8 @@ def main(args=sys.argv[1:]): action="store_true", dest="pretty", default=False) parser.add_option('-v', help="Verbose output", action="store_true", dest="verbose", default=False) + parser.add_option('--version', help="Display the current version information", + action="store_true") parser.add_option('--help', help='show this help message', action="store_true") @@ -53,6 +55,10 @@ def main(args=sys.argv[1:]): parser.print_help() return + if options.version: + print(openphoto.__version__) + return + if args: parser.error("Unknown argument: %s" % args) @@ -64,10 +70,10 @@ def main(args=sys.argv[1:]): # Host option overrides config file settings if options.host: - client = OpenPhoto(host=options.host) + client = openphoto.OpenPhoto(host=options.host) else: try: - client = OpenPhoto(config_file=options.config_file) + client = openphoto.OpenPhoto(config_file=options.config_file) except IOError as error: print(error) print(CONFIG_ERROR) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index c33c47b..ea182ee 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -23,7 +23,7 @@ def raise_exception(_): class TestCli(unittest.TestCase): test_file = os.path.join("tests", "unit", "data", "test_file.txt") - @mock.patch.object(openphoto.main, "OpenPhoto") + @mock.patch.object(openphoto.main.openphoto, "OpenPhoto") @mock.patch('sys.stdout', new_callable=io.StringIO) def test_defaults(self, _, mock_openphoto): """Check that the default behaviour is correct""" @@ -32,14 +32,14 @@ class TestCli(unittest.TestCase): mock_openphoto.assert_called_with(config_file=None) get.assert_called_with("/photos/list.json", process_response=False) - @mock.patch.object(openphoto.main, "OpenPhoto") + @mock.patch.object(openphoto.main.openphoto, "OpenPhoto") @mock.patch('sys.stdout', new_callable=io.StringIO) def test_config(self, _, mock_openphoto): """Check that a config file can be specified""" main(["--config=test"]) mock_openphoto.assert_called_with(config_file="test") - @mock.patch.object(openphoto.main, "OpenPhoto") + @mock.patch.object(openphoto.main.openphoto, "OpenPhoto") @mock.patch('sys.stdout', new_callable=io.StringIO) def test_get(self, mock_stdout, mock_openphoto): """Check that the get operation is working""" @@ -52,7 +52,7 @@ class TestCli(unittest.TestCase): process_response=False) self.assertEqual(mock_stdout.getvalue(), "Result\n") - @mock.patch.object(openphoto.main, "OpenPhoto") + @mock.patch.object(openphoto.main.openphoto, "OpenPhoto") @mock.patch('sys.stdout', new_callable=io.StringIO) def test_post(self, mock_stdout, mock_openphoto): """Check that the post operation is working""" @@ -65,7 +65,7 @@ class TestCli(unittest.TestCase): files={}, process_response=False) self.assertEqual(mock_stdout.getvalue(), "Result\n") - @mock.patch.object(openphoto.main, "OpenPhoto") + @mock.patch.object(openphoto.main.openphoto, "OpenPhoto") @mock.patch('sys.stdout', new_callable=io.StringIO) def test_post_files(self, _, mock_openphoto): """Check that files are posted correctly""" @@ -104,7 +104,7 @@ class TestCli(unittest.TestCase): mock_stdout.getvalue()) self.assertIn("To get your credentials", mock_stdout.getvalue()) - @mock.patch.object(openphoto.main, "OpenPhoto") + @mock.patch.object(openphoto.main.openphoto, "OpenPhoto") @mock.patch('sys.stdout', new_callable=io.StringIO) def test_verbose(self, mock_stdout, _): """Check that the verbose option is working""" @@ -112,7 +112,7 @@ class TestCli(unittest.TestCase): self.assertIn("Method: GET", mock_stdout.getvalue()) self.assertIn("Endpoint: /photos/list.json", mock_stdout.getvalue()) - @mock.patch.object(openphoto.main, "OpenPhoto") + @mock.patch.object(openphoto.main.openphoto, "OpenPhoto") @mock.patch('sys.stdout', new_callable=io.StringIO) def test_pretty_print(self, mock_stdout, mock_openphoto): """Check that the pretty-print option is working""" @@ -120,3 +120,10 @@ class TestCli(unittest.TestCase): get.return_value = '{"test":1}' main(["-p"]) self.assertEqual(mock_stdout.getvalue(), '{\n "test":1\n}\n') + + @mock.patch('sys.stdout', new_callable=io.StringIO) + def test_version(self, mock_stdout): + """Check that the version string is correctly printed""" + main(["--version"]) + self.assertEqual(mock_stdout.getvalue(), openphoto.__version__ + "\n") + From 40410d08e28c2f7d1c5cc25cf11696dbd951f6e8 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 6 Jul 2013 14:05:54 +0100 Subject: [PATCH 44/58] Updated metadata --- setup.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index b978a24..be52e55 100755 --- a/setup.py +++ b/setup.py @@ -1,13 +1,21 @@ #!/usr/bin/env python +import sys import openphoto requires = ['requests', 'requests_oauthlib'] +console_script = """[console_scripts] +openphoto = openphoto.main:main +""" +# Check the Python version +(major, minor) = sys.version_info[:2] +if (major, minor) < (2, 6): + raise SystemExit("Sorry, Python 2.6 or newer required") + try: from setuptools import setup - kw = {'entry_points': - """[console_scripts]\nopenphoto = openphoto.main:main\n""", - 'zip_safe': False, + kw = {'entry_points': console_script, + 'zip_safe': True, 'install_requires': requires } except ImportError: @@ -17,9 +25,23 @@ except ImportError: setup(name='openphoto', version=openphoto.__version__, - description='Python client library for Trovebox/Openphoto', + description='The official Python client library for Trovebox/OpenPhoto', + long_description=('This library works with any OpenPhoto server ' + '(including the trovebox.com hosted service).\n' + 'It provides full access to your photos and metadata, ' + 'via a simple Pythonic API.'), author='Pete Burgers, James Walker', url='https://github.com/openphoto/openphoto-python', packages=['openphoto'], + classifiers=['Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Multimedia :: Graphics', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + license='LICENSE', + test_suite='tests.unit', **kw ) From 51ccea9761e8799bbd099878eafb2739f24af766 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 6 Jul 2013 14:34:08 +0100 Subject: [PATCH 45/58] Update metadata --- setup.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index be52e55..c23f09c 100755 --- a/setup.py +++ b/setup.py @@ -26,13 +26,11 @@ except ImportError: setup(name='openphoto', version=openphoto.__version__, description='The official Python client library for Trovebox/OpenPhoto', - long_description=('This library works with any OpenPhoto server ' - '(including the trovebox.com hosted service).\n' - 'It provides full access to your photos and metadata, ' - 'via a simple Pythonic API.'), + long_description=open("README.markdown").read(), author='Pete Burgers, James Walker', url='https://github.com/openphoto/openphoto-python', packages=['openphoto'], + keywords=['openphoto', 'pyopenphoto', 'openphoto-python', 'trovebox'], classifiers=['Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', @@ -41,7 +39,7 @@ setup(name='openphoto', 'Topic :: Multimedia :: Graphics', 'Topic :: Software Development :: Libraries :: Python Modules', ], - license='LICENSE', + license='Apache 2.0', test_suite='tests.unit', **kw ) From 535bdc101bf1eb4195edb839a4ab9fffdbf32557 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 6 Jul 2013 15:15:01 +0100 Subject: [PATCH 46/58] Converted README to ReStructuredText, required for PyPI. --- README.markdown => README.rst | 102 +++++++++++++++++----------------- 1 file changed, 52 insertions(+), 50 deletions(-) rename README.markdown => README.rst (62%) diff --git a/README.markdown b/README.rst similarity index 62% rename from README.markdown rename to README.rst index 206ed9f..e28b137 100644 --- a/README.markdown +++ b/README.rst @@ -1,18 +1,30 @@ -Open Photo API / Python Library -======================= -#### OpenPhoto, a photo service for the masses -[![Build Status](https://api.travis-ci.org/photo/openphoto-python.png)](https://travis-ci.org/photo/openphoto-python) +================================= +OpenPhoto/Trovebox Python Library +================================= +.. image:: https://api.travis-ci.org/photo/openphoto-python.png + :alt: Build Status + :target: https://travis-ci.org/photo/openphoto-python ----------------------------------------- - -### Installation - python setup.py install +This library works with any `OpenPhoto `__ server +(including the `Trovebox `__ hosted service). +It provides full access to your photos and metadata, via a simple +Pythonic API. ----------------------------------------- - -### Credentials +Installation +============ +:: -For full access to your photos, you need to create the following config file in ``~/.config/openphoto/default`` + pip install openphoto + +Documentation +============= +See the `OpenPhoto/Trovebox API Documentation `__ +for full API documentation, including Python examples. + +Credentials +=========== +For full access to your photos, you need to create the following config +file in ``~/.config/openphoto/default``:: # ~/.config/openphoto/default host = your.host.com @@ -24,30 +36,15 @@ For full access to your photos, you need to create the following config file in The ``config_file`` switch lets you specify a different config file. To get your credentials: - * Log into your Trovebox site - * Click the arrow on the top-right and select 'Settings' - * Click the 'Create a new app' button - * Click the 'View' link beside the newly created app ----------------------------------------- - -### How to use the library +* Log into your Trovebox site +* Click the arrow on the top-right and select 'Settings' +* Click the 'Create a new app' button +* Click the 'View' link beside the newly created app -You can use the library in one of two ways: - - * Direct GET/POST calls to the server - * Access via Python classes/methods - - -#### Direct GET/POST: - - from openphoto import OpenPhoto - client = OpenPhoto() - resp = client.get("/photos/list.json") - resp = client.post("/photo/62/update.json", tags=["tag1", "tag2"]) - - -#### Python classes/methods +Using the library +================= +:: from openphoto import OpenPhoto client = OpenPhoto() @@ -55,30 +52,33 @@ You can use the library in one of two ways: photos[0].update(tags=["tag1", "tag2"]) print(photos[0].tags) -The OpenPhoto Python class hierarchy mirrors the [OpenPhoto API](http://theopenphotoproject.org/documentation) endpoint layout. For example, the calls in the example above use the following API endpoints: +The OpenPhoto Python class hierarchy mirrors the +`OpenPhoto/Trovebox API `__ endpoint layout. +For example, the calls in the example above use the following API endpoints: * ``client.photos.list() -> /photos/list.json`` * ``photos[0].update() -> /photo//update.json`` - -### API Versioning +You can also access the API at a lower level using GET/POST methods:: + resp = client.get("/photos/list.json") + resp = client.post("/photo/62/update.json", tags=["tag1", "tag2"]) + +API Versioning +============== It may be useful to lock your application to a particular version of the OpenPhoto API. This ensures that future API updates won't cause unexpected breakages. -To do this, add the optional ```api_version``` parameter when creating the client object: +To do this, add the optional ```api_version``` parameter when creating the client object:: from openphoto import OpenPhoto client = OpenPhoto(api_version=2) ----------------------------------------- - - -### Using from the command line - +Commandline Tool +================ You can run commands to the OpenPhoto API from your shell! -These are the options you can pass to the shell program: +These are the options you can pass to the shell program:: --help # Display help text -c config_file # Either the name of a config file in ~/.config/openphoto/ or a full path to a config file @@ -88,11 +88,12 @@ These are the options you can pass to the shell program: -F params # e.g. -F 'title=my title' -F 'tags=mytag1,mytag2' -p # Pretty print the json -v # Verbose output + --version # Display the current version information - -#### Command line examples +Commandline Examples +-------------------- +Upload a public photo to the host specified in ```~/.config/openphoto/default```:: - # Upload a public photo to the host specified in ~/.config/openphoto/default openphoto -p -X POST -e /photo/upload.json -F 'photo=@/path/to/photo/jpg' -F 'permission=1' { "code":201, @@ -104,8 +105,9 @@ These are the options you can pass to the shell program: ... } } - - # Get a thumbnail URL from current.openphoto.me (unauthenticated access) + +Get a thumbnail URL from current.openphoto.me (unauthenticated access):: + openphoto -h current.openphoto.me -p -e /photo/62/view.json -F 'returnSizes=20x20' { "code":200, @@ -128,4 +130,4 @@ These are the options you can pass to the shell program: ... ... } - } + } From ee5e5059162463d5a341aa572c94840e25d33b54 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 6 Jul 2013 15:26:41 +0100 Subject: [PATCH 47/58] Upate to use README.rst --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c23f09c..b1df50a 100755 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ except ImportError: setup(name='openphoto', version=openphoto.__version__, description='The official Python client library for Trovebox/OpenPhoto', - long_description=open("README.markdown").read(), + long_description=open("README.rst").read(), author='Pete Burgers, James Walker', url='https://github.com/openphoto/openphoto-python', packages=['openphoto'], From 350ac7bbcb488af00b43f5c04210efb6ce5385a8 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 6 Jul 2013 15:45:53 +0100 Subject: [PATCH 48/58] Fix Github URL --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b1df50a..7ac96fd 100755 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup(name='openphoto', description='The official Python client library for Trovebox/OpenPhoto', long_description=open("README.rst").read(), author='Pete Burgers, James Walker', - url='https://github.com/openphoto/openphoto-python', + url='https://github.com/photo/openphoto-python', packages=['openphoto'], keywords=['openphoto', 'pyopenphoto', 'openphoto-python', 'trovebox'], classifiers=['Development Status :: 4 - Beta', From 375fbf5556d2d83b067693569c798d428ef560dd Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 6 Jul 2013 15:46:37 +0100 Subject: [PATCH 49/58] Update README with PyPI and GitHub links --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index e28b137..a15c84b 100644 --- a/README.rst +++ b/README.rst @@ -5,6 +5,10 @@ OpenPhoto/Trovebox Python Library :alt: Build Status :target: https://travis-ci.org/photo/openphoto-python +.. image:: https://pypip.in/v/openphoto/badge.png + :alt: Python Package Index (PyPI) + :target: https://pypi.python.org/pypi/openphoto + This library works with any `OpenPhoto `__ server (including the `Trovebox `__ hosted service). It provides full access to your photos and metadata, via a simple @@ -21,6 +25,8 @@ Documentation See the `OpenPhoto/Trovebox API Documentation `__ for full API documentation, including Python examples. +All development takes place at the `openphoto-python GitHub site `__. + Credentials =========== For full access to your photos, you need to create the following config From 11d580aae1c9e9bb42485245c4fbd5c96dffc9a4 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 7 Jul 2013 10:22:53 +0100 Subject: [PATCH 50/58] Read version into setup.py without importing, to avoid dependency problems --- openphoto/__init__.py | 3 +-- openphoto/_version.py | 1 + setup.py | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 openphoto/_version.py diff --git a/openphoto/__init__.py b/openphoto/__init__.py index 07b07a1..ab4d804 100644 --- a/openphoto/__init__.py +++ b/openphoto/__init__.py @@ -1,11 +1,10 @@ from openphoto.openphoto_http import OpenPhotoHttp from openphoto.errors import * +from openphoto._version import __version__ import openphoto.api_photo import openphoto.api_tag import openphoto.api_album -__version__ = "0.3" - LATEST_API_VERSION = 2 class OpenPhoto(OpenPhotoHttp): diff --git a/openphoto/_version.py b/openphoto/_version.py new file mode 100644 index 0000000..6a35e85 --- /dev/null +++ b/openphoto/_version.py @@ -0,0 +1 @@ +__version__ = "0.3" diff --git a/setup.py b/setup.py index 7ac96fd..26ab50a 100755 --- a/setup.py +++ b/setup.py @@ -1,12 +1,15 @@ #!/usr/bin/env python import sys -import openphoto requires = ['requests', 'requests_oauthlib'] console_script = """[console_scripts] openphoto = openphoto.main:main """ + +# from openphoto._version import __version__ +exec(open("openphoto/_version.py").read()) + # Check the Python version (major, minor) = sys.version_info[:2] if (major, minor) < (2, 6): @@ -24,7 +27,7 @@ except ImportError: 'requires': requires} setup(name='openphoto', - version=openphoto.__version__, + version=__version__, description='The official Python client library for Trovebox/OpenPhoto', long_description=open("README.rst").read(), author='Pete Burgers, James Walker', From 8a187012e102020793c18e13da0f984da5c10c7a Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sun, 7 Jul 2013 10:34:00 +0100 Subject: [PATCH 51/58] Bump version to 0.4 --- openphoto/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openphoto/_version.py b/openphoto/_version.py index 6a35e85..896a370 100644 --- a/openphoto/_version.py +++ b/openphoto/_version.py @@ -1 +1 @@ -__version__ = "0.3" +__version__ = "0.4" From ce1d10ee2aa04ba4d1ab3178b94ce633d15dc4a4 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Tue, 9 Jul 2013 21:25:58 +0100 Subject: [PATCH 52/58] Ensure README.rst is zipped up into sdist --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 26ab50a..b795cc6 100755 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ setup(name='openphoto', author='Pete Burgers, James Walker', url='https://github.com/photo/openphoto-python', packages=['openphoto'], + data_files=['README.rst'], keywords=['openphoto', 'pyopenphoto', 'openphoto-python', 'trovebox'], classifiers=['Development Status :: 4 - Beta', 'Intended Audience :: Developers', From 5cfff7b4ba6674945c11d696ac53180576c6f0be Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Fri, 19 Jul 2013 08:06:48 +0100 Subject: [PATCH 53/58] Remove excessive backticks --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a15c84b..9988804 100644 --- a/README.rst +++ b/README.rst @@ -75,7 +75,7 @@ API Versioning It may be useful to lock your application to a particular version of the OpenPhoto API. This ensures that future API updates won't cause unexpected breakages. -To do this, add the optional ```api_version``` parameter when creating the client object:: +To do this, add the optional ``api_version`` parameter when creating the client object:: from openphoto import OpenPhoto client = OpenPhoto(api_version=2) From 57b593d24556ebe9242abbfa9851a91f05aa1882 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Fri, 19 Jul 2013 16:57:53 +0100 Subject: [PATCH 54/58] Add v3.0.8 functional test instance to server list --- run_functional_tests | 13 ++++++++++++- tests/functional/README.markdown | 5 +++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/run_functional_tests b/run_functional_tests index 5208de8..d314dac 100755 --- a/run_functional_tests +++ b/run_functional_tests @@ -4,7 +4,7 @@ # # Test server running latest self-hosted site - +# Install from latest photo/frontend master commit tput setaf 3 echo echo "Testing latest self-hosted site..." @@ -14,6 +14,7 @@ unset OPENPHOTO_TEST_SERVER_API python -m unittest discover --catch tests/functional # Test server running APIv1 OpenPhoto instance +# Install from photo/frontend commit 660b2ab tput setaf 3 echo echo "Testing APIv1 self-hosted site..." @@ -22,6 +23,16 @@ export OPENPHOTO_TEST_CONFIG=test-apiv1 export OPENPHOTO_TEST_SERVER_API=1 python -m unittest discover --catch tests/functional +# Test server running v3.0.8 OpenPhoto instance +# Install from photo/frontend commit e9d81de57b +tput setaf 3 +echo +echo "Testing v3.0.8 self-hosted site..." +tput sgr0 +export OPENPHOTO_TEST_CONFIG=test-3.0.8 +unset OPENPHOTO_TEST_SERVER_API +python -m unittest discover --catch tests/functional + # Test account on hosted trovebox.com site tput setaf 3 echo diff --git a/tests/functional/README.markdown b/tests/functional/README.markdown index 9f4f804..3fdd552 100644 --- a/tests/functional/README.markdown +++ b/tests/functional/README.markdown @@ -99,6 +99,7 @@ all supported API versions. To use it, you must set up multiple OpenPhoto instances and create the following config files containing your credentials: - test : Latest self-hosted site - test-apiv1 : APIv1 self-hosted site + test : Latest self-hosted site (from photo/frontend master branch) + test-apiv1 : APIv1 self-hosted site (from photo/frontend commit 660b2ab) + test-3.0.8 : v3.0.8 self-hosted site (from photo/frontend commit e9d81de57b) test-hosted : Credentials for test account on trovebox.com From 6f70330994fd629093406dac77e74125c6fbe513 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Fri, 19 Jul 2013 17:46:30 +0100 Subject: [PATCH 55/58] Renamed OpenPhoto to Trovebox --- README.rst | 49 +++++----- bin/openphoto | 4 - bin/trovebox | 4 + openphoto/__init__.py | 35 ------- run_functional_tests | 20 ++-- setup.py | 17 ++-- tests/README.markdown | 6 +- tests/functional/README.markdown | 30 +++--- tests/functional/test_albums.py | 4 +- tests/functional/test_base.py | 12 +-- tests/functional/test_framework.py | 21 ++-- tests/functional/test_photos.py | 14 +-- tests/functional/test_tags.py | 10 +- tests/unit/test_albums.py | 56 +++++------ tests/unit/test_cli.py | 50 +++++----- tests/unit/test_config.py | 18 ++-- tests/unit/test_http.py | 24 ++--- tests/unit/test_http_errors.py | 26 ++--- tests/unit/test_photos.py | 96 +++++++++---------- tests/unit/test_tags.py | 28 +++--- trovebox/__init__.py | 35 +++++++ {openphoto => trovebox}/_version.py | 0 {openphoto => trovebox}/api_album.py | 4 +- {openphoto => trovebox}/api_photo.py | 18 ++-- {openphoto => trovebox}/api_tag.py | 4 +- {openphoto => trovebox}/config.py | 2 +- {openphoto => trovebox}/errors.py | 8 +- .../openphoto_http.py => trovebox/http.py | 40 ++++---- {openphoto => trovebox}/main.py | 12 +-- {openphoto => trovebox}/objects.py | 86 ++++++++--------- 30 files changed, 368 insertions(+), 365 deletions(-) delete mode 100755 bin/openphoto create mode 100755 bin/trovebox delete mode 100644 openphoto/__init__.py create mode 100644 trovebox/__init__.py rename {openphoto => trovebox}/_version.py (100%) rename {openphoto => trovebox}/api_album.py (95%) rename {openphoto => trovebox}/api_photo.py (90%) rename {openphoto => trovebox}/api_tag.py (93%) rename {openphoto => trovebox}/config.py (97%) rename {openphoto => trovebox}/errors.py (54%) rename openphoto/openphoto_http.py => trovebox/http.py (86%) rename {openphoto => trovebox}/main.py (93%) rename {openphoto => trovebox}/objects.py (71%) diff --git a/README.rst b/README.rst index 9988804..5f8f24b 100644 --- a/README.rst +++ b/README.rst @@ -5,12 +5,13 @@ OpenPhoto/Trovebox Python Library :alt: Build Status :target: https://travis-ci.org/photo/openphoto-python -.. image:: https://pypip.in/v/openphoto/badge.png +.. image:: https://pypip.in/v/trovebox/badge.png :alt: Python Package Index (PyPI) - :target: https://pypi.python.org/pypi/openphoto + :target: https://pypi.python.org/pypi/trovebox -This library works with any `OpenPhoto `__ server -(including the `Trovebox `__ hosted service). +This library works with any Trovebox server, either +`Self-hosted `__, or using the +`Trovebox hosted service`__). It provides full access to your photos and metadata, via a simple Pythonic API. @@ -18,11 +19,11 @@ Installation ============ :: - pip install openphoto + pip install trovebox Documentation ============= -See the `OpenPhoto/Trovebox API Documentation `__ +See the `Trovebox API Documentation `__ for full API documentation, including Python examples. All development takes place at the `openphoto-python GitHub site `__. @@ -30,9 +31,9 @@ All development takes place at the `openphoto-python GitHub site `__ endpoint layout. +The Trovebox Python class hierarchy mirrors the +`Trovebox API `__ endpoint layout. For example, the calls in the example above use the following API endpoints: * ``client.photos.list() -> /photos/list.json`` @@ -72,22 +73,22 @@ You can also access the API at a lower level using GET/POST methods:: API Versioning ============== -It may be useful to lock your application to a particular version of the OpenPhoto API. +It may be useful to lock your application to a particular version of the Trovebox API. This ensures that future API updates won't cause unexpected breakages. To do this, add the optional ``api_version`` parameter when creating the client object:: - from openphoto import OpenPhoto - client = OpenPhoto(api_version=2) + from trovebox import Trovebox + client = Trovebox(api_version=2) Commandline Tool ================ -You can run commands to the OpenPhoto API from your shell! +You can run commands to the Trovebox API from your shell! These are the options you can pass to the shell program:: --help # Display help text - -c config_file # Either the name of a config file in ~/.config/openphoto/ or a full path to a config file + -c config_file # Either the name of a config file in ~/.config/trovebox/ or a full path to a config file -h hostname # Overrides config_file for unauthenticated API calls -e endpoint # [default=/photos/list.json] -X method # [default=GET] @@ -98,9 +99,9 @@ These are the options you can pass to the shell program:: Commandline Examples -------------------- -Upload a public photo to the host specified in ```~/.config/openphoto/default```:: +Upload a public photo to the host specified in ```~/.config/trovebox/default```:: - openphoto -p -X POST -e /photo/upload.json -F 'photo=@/path/to/photo/jpg' -F 'permission=1' + trovebox -p -X POST -e /photo/upload.json -F 'photo=@/path/to/photo/jpg' -F 'permission=1' { "code":201, "message":"Photo 1eo uploaded successfully", @@ -112,9 +113,9 @@ Upload a public photo to the host specified in ```~/.config/openphoto/default``` } } -Get a thumbnail URL from current.openphoto.me (unauthenticated access):: +Get a thumbnail URL from current.trovebox.com (unauthenticated access):: - openphoto -h current.openphoto.me -p -e /photo/62/view.json -F 'returnSizes=20x20' + openphoto -h current.trovebox.com -p -e /photo/62/view.json -F 'returnSizes=20x20' { "code":200, "message":"Photo 62", @@ -125,11 +126,11 @@ Get a thumbnail URL from current.openphoto.me (unauthenticated access):: ], ... ... - "path20x20":"http://current.openphoto.me/photo/62/create/36c0a/20x20.jpg", - "pathBase":"http://awesomeness.openphoto.me/base/201203/7ae997-Boracay-Philippines-007.jpg", + "path20x20":"http://current.trovebox.com/photo/62/create/36c0a/20x20.jpg", + "pathBase":"http://awesomeness.trovebox.com/base/201203/7ae997-Boracay-Philippines-007.jpg", "permission":"1", "photo20x20":[ - "http://current.openphoto.me/photo/62/create/36c0a/20x20.jpg", + "http://current.trovebox.com/photo/62/create/36c0a/20x20.jpg", 13, 20 ], diff --git a/bin/openphoto b/bin/openphoto deleted file mode 100755 index ec299d7..0000000 --- a/bin/openphoto +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env python - -import openphoto.main -openphoto.main.main() diff --git a/bin/trovebox b/bin/trovebox new file mode 100755 index 0000000..6c63d30 --- /dev/null +++ b/bin/trovebox @@ -0,0 +1,4 @@ +#!/usr/bin/env python + +import trovebox.main +trovebox.main.main() diff --git a/openphoto/__init__.py b/openphoto/__init__.py deleted file mode 100644 index ab4d804..0000000 --- a/openphoto/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -from openphoto.openphoto_http import OpenPhotoHttp -from openphoto.errors import * -from openphoto._version import __version__ -import openphoto.api_photo -import openphoto.api_tag -import openphoto.api_album - -LATEST_API_VERSION = 2 - -class OpenPhoto(OpenPhotoHttp): - """ - Client library for OpenPhoto - If no parameters are specified, config is loaded from the default - location (~/.config/openphoto/default). - The config_file parameter is used to specify an alternate config file. - If the host parameter is specified, no config file is loaded and - OAuth tokens (consumer*, token*) can optionally be specified. - All requests will include the api_version path, if specified. - This should be used to ensure that your application will continue to work - even if the OpenPhoto API is updated to a new revision. - """ - def __init__(self, config_file=None, host=None, - consumer_key='', consumer_secret='', - token='', token_secret='', - api_version=None): - OpenPhotoHttp.__init__(self, config_file, host, - consumer_key, consumer_secret, - token, token_secret, api_version) - - self.photos = openphoto.api_photo.ApiPhotos(self) - self.photo = openphoto.api_photo.ApiPhoto(self) - self.tags = openphoto.api_tag.ApiTags(self) - self.tag = openphoto.api_tag.ApiTag(self) - self.albums = openphoto.api_album.ApiAlbums(self) - self.album = openphoto.api_album.ApiAlbum(self) diff --git a/run_functional_tests b/run_functional_tests index d314dac..ee3c8fa 100755 --- a/run_functional_tests +++ b/run_functional_tests @@ -9,28 +9,28 @@ tput setaf 3 echo echo "Testing latest self-hosted site..." tput sgr0 -export OPENPHOTO_TEST_CONFIG=test -unset OPENPHOTO_TEST_SERVER_API +export TROVEBOX_TEST_CONFIG=test +unset TROVEBOX_TEST_SERVER_API python -m unittest discover --catch tests/functional -# Test server running APIv1 OpenPhoto instance +# Test server running APIv1 Trovebox instance # Install from photo/frontend commit 660b2ab tput setaf 3 echo echo "Testing APIv1 self-hosted site..." tput sgr0 -export OPENPHOTO_TEST_CONFIG=test-apiv1 -export OPENPHOTO_TEST_SERVER_API=1 +export TROVEBOX_TEST_CONFIG=test-apiv1 +export TROVEBOX_TEST_SERVER_API=1 python -m unittest discover --catch tests/functional -# Test server running v3.0.8 OpenPhoto instance +# Test server running v3.0.8 Trovebox instance # Install from photo/frontend commit e9d81de57b tput setaf 3 echo echo "Testing v3.0.8 self-hosted site..." tput sgr0 -export OPENPHOTO_TEST_CONFIG=test-3.0.8 -unset OPENPHOTO_TEST_SERVER_API +export TROVEBOX_TEST_CONFIG=test-3.0.8 +unset TROVEBOX_TEST_SERVER_API python -m unittest discover --catch tests/functional # Test account on hosted trovebox.com site @@ -38,7 +38,7 @@ tput setaf 3 echo echo "Testing latest hosted site..." tput sgr0 -export OPENPHOTO_TEST_CONFIG=test-hosted -unset OPENPHOTO_TEST_SERVER_API +export TROVEBOX_TEST_CONFIG=test-hosted +unset TROVEBOX_TEST_SERVER_API python -m unittest discover --catch tests/functional diff --git a/setup.py b/setup.py index b795cc6..4f71e1c 100755 --- a/setup.py +++ b/setup.py @@ -4,11 +4,11 @@ import sys requires = ['requests', 'requests_oauthlib'] console_script = """[console_scripts] -openphoto = openphoto.main:main +trovebox = trovebox.main:main """ -# from openphoto._version import __version__ -exec(open("openphoto/_version.py").read()) +# from trovebox._version import __version__ +exec(open("trovebox/_version.py").read()) # Check the Python version (major, minor) = sys.version_info[:2] @@ -23,18 +23,19 @@ try: } except ImportError: from distutils.core import setup - kw = {'scripts': ['bin/openphoto'], + kw = {'scripts': ['bin/trovebox'], 'requires': requires} -setup(name='openphoto', +setup(name='trovebox', version=__version__, - description='The official Python client library for Trovebox/OpenPhoto', + description='The official Python client library for the Trovebox photo service', long_description=open("README.rst").read(), author='Pete Burgers, James Walker', url='https://github.com/photo/openphoto-python', - packages=['openphoto'], + packages=['trovebox'], data_files=['README.rst'], - keywords=['openphoto', 'pyopenphoto', 'openphoto-python', 'trovebox'], + keywords=['openphoto', 'pyopenphoto', 'openphoto-python', + 'trovebox', 'pytrovebox', 'openphoto-trovebox'], classifiers=['Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', diff --git a/tests/README.markdown b/tests/README.markdown index 793ae68..3aa3d35 100644 --- a/tests/README.markdown +++ b/tests/README.markdown @@ -1,4 +1,4 @@ -OpenPhoto/Trovebox Python Testing +Trovebox Python Testing ======================= ###Unit Tests @@ -26,8 +26,8 @@ To run the unit tests against all supported Python versions, use ```tox```: ###Functional Tests -The functional tests check that the openphoto-python library interoperates -correctly with a real OpenPhoto/Trovebox server. +The functional tests check that the Trovebox python library interoperates +correctly with a real Trovebox server. They are slow to run and rely on a stable HTTP connection to a test server. diff --git a/tests/functional/README.markdown b/tests/functional/README.markdown index 3fdd552..5a22abc 100644 --- a/tests/functional/README.markdown +++ b/tests/functional/README.markdown @@ -1,34 +1,34 @@ Functional Testing ======================= -These functional tests check that the openphoto-python library interoperates -correctly with a real OpenPhoto/Trovebox server. +These functional tests check that the Trovebox python library interoperates +correctly with a real Trovebox server. They are slow to run, and require a stable HTTP connection to a test server. ---------------------------------------- ### Requirements -A computer, Python and an empty OpenPhoto/Trovebox test host. +A computer, Python and an empty Trovebox test host. --------------------------------------- ### Setting up -Create a ``~/.config/openphoto/test`` config file containing the following: +Create a ``~/.config/trovebox/test`` config file containing the following: - # ~/.config/openphoto/test + # ~/.config/trovebox/test host = your.host.com consumerKey = your_consumer_key consumerSecret = your_consumer_secret token = your_access_token tokenSecret = your_access_token_secret -Make sure this is an empty test server, **not a production OpenPhoto server!!!** +Make sure this is an empty test server, **not a production Trovebox server!!!** You can specify an alternate test config file with the following environment variable: - export OPENPHOTO_TEST_CONFIG=test2 + export TROVEBOX_TEST_CONFIG=test2 --------------------------------------- @@ -37,28 +37,28 @@ You can specify an alternate test config file with the following environment var The following instructions are for Python 2.7. You can adapt them for earlier Python versions using the ``unittest2`` package. - cd /path/to/openphoto-python + cd /path/to/trovebox-python python -m unittest discover -c tests/functional The "-c" lets you stop the tests gracefully with \[CTRL\]-c. The easiest way to run a subset of the tests is with the ``nose`` package: - cd /path/to/openphoto-python + cd /path/to/trovebox-python nosetests -v -s --nologcapture tests/functional/test_albums.py:TestAlbums.test_view All HTTP requests and responses are recorded in the file ``tests.log``. You can enable more verbose output to stdout with the following environment variable: - export OPENPHOTO_TEST_DEBUG=1 + export TROVEBOX_TEST_DEBUG=1 --------------------------------------- ### Test Details -These tests are intended to verify the openphoto-python library. -They don't provide comprehensive testing of the OpenPhoto API, +These tests are intended to verify the Trovebox python library. +They don't provide comprehensive testing of the Trovebox API, there are PHP unit tests for that. Each test class is run as follows: @@ -84,11 +84,11 @@ Remove all photos, tags and albums By default, all currently supported API versions will be tested. It's useful to test servers that only support older API versions. To restrict the testing to a specific maximum API version, use the -``OPENPHOTO_TEST_SERVER_API`` environment variable. +``TROVEBOX_TEST_SERVER_API`` environment variable. For example, to restrict testing to APIv1 and APIv2: - export OPENPHOTO_TEST_SERVER_API=2 + export TROVEBOX_TEST_SERVER_API=2 ### Full Regression Test @@ -96,7 +96,7 @@ For example, to restrict testing to APIv1 and APIv2: The ``run_functional_tests`` script runs all functional tests against all supported API versions. -To use it, you must set up multiple OpenPhoto instances and create the following +To use it, you must set up multiple Trovebox instances and create the following config files containing your credentials: test : Latest self-hosted site (from photo/frontend master branch) diff --git a/tests/functional/test_albums.py b/tests/functional/test_albums.py index 95b28ab..2b1b21f 100644 --- a/tests/functional/test_albums.py +++ b/tests/functional/test_albums.py @@ -29,7 +29,7 @@ class TestAlbums(test_base.TestBase): def test_update(self): """ Test that an album can be updated """ - # Update the album using the OpenPhoto class, + # Update the album using the Trovebox class, # passing in the album object new_name = "New Name" self.client.album.update(self.albums[0], name=new_name) @@ -38,7 +38,7 @@ class TestAlbums(test_base.TestBase): self.albums = self.client.albums.list() self.assertEqual(self.albums[0].name, new_name) - # Update the album using the OpenPhoto class, passing in the album id + # Update the album using the Trovebox class, passing in the album id new_name = "Another New Name" self.client.album.update(self.albums[0].id, name=new_name) diff --git a/tests/functional/test_base.py b/tests/functional/test_base.py index 2c3926e..9c7056c 100644 --- a/tests/functional/test_base.py +++ b/tests/functional/test_base.py @@ -7,11 +7,11 @@ try: except ImportError: import unittest -import openphoto +import trovebox def get_test_server_api(): - return int(os.getenv("OPENPHOTO_TEST_SERVER_API", - openphoto.LATEST_API_VERSION)) + return int(os.getenv("TROVEBOX_TEST_SERVER_API", + trovebox.LATEST_API_VERSION)) class TestBase(unittest.TestCase): TEST_TITLE = "Test Image - delete me!" @@ -21,8 +21,8 @@ class TestBase(unittest.TestCase): testcase_name = "(unknown testcase)" api_version = None - config_file = os.getenv("OPENPHOTO_TEST_CONFIG", "test") - debug = (os.getenv("OPENPHOTO_TEST_DEBUG", "0") == "1") + config_file = os.getenv("TROVEBOX_TEST_CONFIG", "test") + debug = (os.getenv("TROVEBOX_TEST_DEBUG", "0") == "1") def __init__(self, *args, **kwds): super(TestBase, self).__init__(*args, **kwds) @@ -42,7 +42,7 @@ class TestBase(unittest.TestCase): else: print("\nTesting %s v%d" % (cls.testcase_name, cls.api_version)) - cls.client = openphoto.OpenPhoto(config_file=cls.config_file, + cls.client = trovebox.Trovebox(config_file=cls.config_file, api_version=cls.api_version) if cls.client.photos.list() != []: diff --git a/tests/functional/test_framework.py b/tests/functional/test_framework.py index 788ac13..8495f0d 100644 --- a/tests/functional/test_framework.py +++ b/tests/functional/test_framework.py @@ -1,6 +1,6 @@ import logging -import openphoto +import trovebox from tests.functional import test_base class TestFramework(test_base.TestBase): @@ -16,8 +16,8 @@ class TestFramework(test_base.TestBase): """ API v0 has a special hello world message """ - client = openphoto.OpenPhoto(config_file=self.config_file, - api_version=0) + client = trovebox.Trovebox(config_file=self.config_file, + api_version=0) result = client.get("hello.json") self.assertEqual(result['message'], "Hello, world! This is version zero of the API!") @@ -28,8 +28,8 @@ class TestFramework(test_base.TestBase): For all API versions >0, we get a generic hello world message """ for api_version in range(1, test_base.get_test_server_api() + 1): - client = openphoto.OpenPhoto(config_file=self.config_file, - api_version=api_version) + client = trovebox.Trovebox(config_file=self.config_file, + api_version=api_version) result = client.get("hello.json") self.assertEqual(result['message'], "Hello, world!") self.assertEqual(result['result']['__route__'], @@ -40,8 +40,8 @@ class TestFramework(test_base.TestBase): If the API version is unspecified, we get a generic hello world message. """ - client = openphoto.OpenPhoto(config_file=self.config_file, - api_version=None) + client = trovebox.Trovebox(config_file=self.config_file, + api_version=None) result = client.get("hello.json") self.assertEqual(result['message'], "Hello, world!") self.assertEqual(result['result']['__route__'], "/hello.json") @@ -51,7 +51,8 @@ class TestFramework(test_base.TestBase): If the API version is unsupported, we should get an error (ValueError, since the returned 404 HTML page is not valid JSON) """ - client = openphoto.OpenPhoto(config_file=self.config_file, - api_version=openphoto.LATEST_API_VERSION + 1) - with self.assertRaises(openphoto.OpenPhoto404Error): + version = trovebox.LATEST_API_VERSION + 1 + client = trovebox.Trovebox(config_file=self.config_file, + api_version=version) + with self.assertRaises(trovebox.Trovebox404Error): client.get("hello.json") diff --git a/tests/functional/test_photos.py b/tests/functional/test_photos.py index c6c9b57..f153b49 100644 --- a/tests/functional/test_photos.py +++ b/tests/functional/test_photos.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -import openphoto +import trovebox from tests.functional import test_base class TestPhotos(test_base.TestBase): @@ -8,9 +8,9 @@ class TestPhotos(test_base.TestBase): def test_delete_upload(self): """ Test photo deletion and upload """ - # Delete one photo using the OpenPhoto class, passing in the id + # Delete one photo using the Trovebox class, passing in the id self.assertTrue(self.client.photo.delete(self.photos[0].id)) - # Delete one photo using the OpenPhoto class, passing in the object + # Delete one photo using the Trovebox class, passing in the object self.assertTrue(self.client.photo.delete(self.photos[1])) # And another using the Photo object directly self.assertTrue(self.photos[2].delete()) @@ -49,7 +49,7 @@ class TestPhotos(test_base.TestBase): def test_edit(self): """ Check that the edit request returns an HTML form """ - # Test using the OpenPhoto class + # Test using the Trovebox class html = self.client.photo.edit(self.photos[0]) self.assertIn(""}) @@ -152,7 +152,7 @@ class TestPhotoEdit(TestPhotos): mock_get.assert_called_with("/photo/1a/edit.json") self.assertEqual(result, "") - @mock.patch.object(openphoto.OpenPhoto, 'get') + @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_edit_id(self, mock_get): """Check that a the photo edit endpoint is working when using an ID""" mock_get.return_value = self._return_value({"markup": ""}) @@ -160,7 +160,7 @@ class TestPhotoEdit(TestPhotos): mock_get.assert_called_with("/photo/1a/edit.json") self.assertEqual(result, "") - @mock.patch.object(openphoto.OpenPhoto, 'get') + @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_object_edit(self, mock_get): """ Check that a the photo edit endpoint is working @@ -172,45 +172,45 @@ class TestPhotoEdit(TestPhotos): self.assertEqual(result, "") class TestPhotoReplace(TestPhotos): - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_replace(self, _): """ If photo.replace gets implemented, write a test! """ with self.assertRaises(NotImplementedError): self.client.photo.replace(self.test_photos[0], self.test_file) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_replace_id(self, _): """ If photo.replace gets implemented, write a test! """ with self.assertRaises(NotImplementedError): self.client.photo.replace("1a", self.test_file) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_object_replace(self, _): """ If photo.replace gets implemented, write a test! """ with self.assertRaises(NotImplementedError): self.test_photos[0].replace(self.test_file) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_replace_encoded(self, _): """ If photo.replace_encoded gets implemented, write a test! """ with self.assertRaises(NotImplementedError): self.client.photo.replace_encoded(self.test_photos[0], self.test_file) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_replace_encoded_id(self, _): """ If photo.replace_encoded gets implemented, write a test! """ with self.assertRaises(NotImplementedError): self.client.photo.replace_encoded("1a", self.test_file) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_object_replace_encoded(self, _): """ If photo.replace_encoded gets implemented, write a test! """ with self.assertRaises(NotImplementedError): self.test_photos[0].replace_encoded(photo_file=self.test_file) class TestPhotoUpdate(TestPhotos): - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_update(self, mock_post): """Check that a photo can be updated""" mock_post.return_value = self._return_value(self.test_photos_dict[1]) @@ -218,7 +218,7 @@ class TestPhotoUpdate(TestPhotos): mock_post.assert_called_with("/photo/1a/update.json", title="Test") self.assertEqual(result.get_fields(), self.test_photos_dict[1]) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_update_id(self, mock_post): """Check that a photo can be updated using its ID""" mock_post.return_value = self._return_value(self.test_photos_dict[1]) @@ -226,7 +226,7 @@ class TestPhotoUpdate(TestPhotos): mock_post.assert_called_with("/photo/1a/update.json", title="Test") self.assertEqual(result.get_fields(), self.test_photos_dict[1]) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_object_update(self, mock_post): """ Check that a photo can be updated @@ -239,7 +239,7 @@ class TestPhotoUpdate(TestPhotos): self.assertEqual(photo.get_fields(), self.test_photos_dict[1]) class TestPhotoView(TestPhotos): - @mock.patch.object(openphoto.OpenPhoto, 'get') + @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_view(self, mock_get): """Check that a photo can be viewed""" mock_get.return_value = self._return_value(self.test_photos_dict[1]) @@ -248,7 +248,7 @@ class TestPhotoView(TestPhotos): mock_get.assert_called_with("/photo/1a/view.json", returnSizes="20x20") self.assertEqual(result.get_fields(), self.test_photos_dict[1]) - @mock.patch.object(openphoto.OpenPhoto, 'get') + @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_view_id(self, mock_get): """Check that a photo can be viewed using its ID""" mock_get.return_value = self._return_value(self.test_photos_dict[1]) @@ -256,7 +256,7 @@ class TestPhotoView(TestPhotos): mock_get.assert_called_with("/photo/1a/view.json", returnSizes="20x20") self.assertEqual(result.get_fields(), self.test_photos_dict[1]) - @mock.patch.object(openphoto.OpenPhoto, 'get') + @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_object_view(self, mock_get): """ Check that a photo can be viewed @@ -269,7 +269,7 @@ class TestPhotoView(TestPhotos): self.assertEqual(photo.get_fields(), self.test_photos_dict[1]) class TestPhotoUpload(TestPhotos): - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_upload(self, mock_post): """Check that a photo can be uploaded""" mock_post.return_value = self._return_value(self.test_photos_dict[0]) @@ -284,7 +284,7 @@ class TestPhotoUpload(TestPhotos): self.assertIn("photo", files) self.assertEqual(result.get_fields(), self.test_photos_dict[0]) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_upload_encoded(self, mock_post): """Check that a photo can be uploaded using Base64 encoding""" mock_post.return_value = self._return_value(self.test_photos_dict[0]) @@ -296,26 +296,26 @@ class TestPhotoUpload(TestPhotos): self.assertEqual(result.get_fields(), self.test_photos_dict[0]) class TestPhotoDynamicUrl(TestPhotos): - @mock.patch.object(openphoto.OpenPhoto, 'get') + @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_dynamic_url(self, _): """ If photo.dynamic_url gets implemented, write a test! """ with self.assertRaises(NotImplementedError): self.client.photo.dynamic_url(self.test_photos[0]) - @mock.patch.object(openphoto.OpenPhoto, 'get') + @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_dynamic_url_id(self, _): """ If photo.dynamic_url gets implemented, write a test! """ with self.assertRaises(NotImplementedError): self.client.photo.dynamic_url("1a") - @mock.patch.object(openphoto.OpenPhoto, 'get') + @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_object_dynamic_url(self, _): """ If photo.dynamic_url gets implemented, write a test! """ with self.assertRaises(NotImplementedError): self.test_photos[0].dynamic_url() class TestPhotoNextPrevious(TestPhotos): - @mock.patch.object(openphoto.OpenPhoto, 'get') + @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_next_previous(self, mock_get): """Check that the next/previous photos are returned""" mock_get.return_value = self._return_value( @@ -328,7 +328,7 @@ class TestPhotoNextPrevious(TestPhotos): self.assertEqual(result["previous"][0].get_fields(), self.test_photos_dict[1]) - @mock.patch.object(openphoto.OpenPhoto, 'get') + @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_next_previous_id(self, mock_get): """ Check that the next/previous photos are returned @@ -344,7 +344,7 @@ class TestPhotoNextPrevious(TestPhotos): self.assertEqual(result["previous"][0].get_fields(), self.test_photos_dict[1]) - @mock.patch.object(openphoto.OpenPhoto, 'get') + @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_object_next_previous(self, mock_get): """ Check that the next/previous photos are returned @@ -360,7 +360,7 @@ class TestPhotoNextPrevious(TestPhotos): self.assertEqual(result["previous"][0].get_fields(), self.test_photos_dict[1]) - @mock.patch.object(openphoto.OpenPhoto, 'get') + @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_next(self, mock_get): """Check that the next photos are returned""" mock_get.return_value = self._return_value( @@ -371,7 +371,7 @@ class TestPhotoNextPrevious(TestPhotos): self.test_photos_dict[0]) self.assertNotIn("previous", result) - @mock.patch.object(openphoto.OpenPhoto, 'get') + @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_previous(self, mock_get): """Check that the previous photos are returned""" mock_get.return_value = self._return_value( @@ -382,7 +382,7 @@ class TestPhotoNextPrevious(TestPhotos): self.test_photos_dict[1]) self.assertNotIn("next", result) - @mock.patch.object(openphoto.OpenPhoto, 'get') + @mock.patch.object(trovebox.Trovebox, 'get') def test_photo_multiple_next_previous(self, mock_get): """Check that multiple next/previous photos are returned""" mock_get.return_value = self._return_value( @@ -400,7 +400,7 @@ class TestPhotoNextPrevious(TestPhotos): self.test_photos_dict[1]) class TestPhotoTransform(TestPhotos): - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_transform(self, mock_post): """Check that a photo can be transformed""" mock_post.return_value = self._return_value(self.test_photos_dict[1]) @@ -408,7 +408,7 @@ class TestPhotoTransform(TestPhotos): mock_post.assert_called_with("/photo/1a/transform.json", rotate="90") self.assertEqual(result.get_fields(), self.test_photos_dict[1]) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_transform_id(self, mock_post): """Check that a photo can be transformed using its ID""" mock_post.return_value = self._return_value(self.test_photos_dict[1]) @@ -416,7 +416,7 @@ class TestPhotoTransform(TestPhotos): mock_post.assert_called_with("/photo/1a/transform.json", rotate="90") self.assertEqual(result.get_fields(), self.test_photos_dict[1]) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_photo_object_transform(self, mock_post): """ Check that a photo can be transformed diff --git a/tests/unit/test_tags.py b/tests/unit/test_tags.py index f915b99..c0248cd 100644 --- a/tests/unit/test_tags.py +++ b/tests/unit/test_tags.py @@ -5,7 +5,7 @@ try: except ImportError: import unittest -import openphoto +import trovebox class TestTags(unittest.TestCase): test_host = "test.example.com" @@ -14,8 +14,8 @@ class TestTags(unittest.TestCase): {"count": 5, "id":"tag2"}] def setUp(self): - self.client = openphoto.OpenPhoto(host=self.test_host) - self.test_tags = [openphoto.objects.Tag(self.client, tag) + self.client = trovebox.Trovebox(host=self.test_host) + self.test_tags = [trovebox.objects.Tag(self.client, tag) for tag in self.test_tags_dict] @staticmethod @@ -23,7 +23,7 @@ class TestTags(unittest.TestCase): return {"message": message, "code": code, "result": result} class TestTagsList(TestTags): - @mock.patch.object(openphoto.OpenPhoto, 'get') + @mock.patch.object(trovebox.Trovebox, 'get') def test_tags_list(self, mock_get): """Check that the the tag list is returned correctly""" mock_get.return_value = self._return_value(self.test_tags_dict) @@ -36,7 +36,7 @@ class TestTagsList(TestTags): self.assertEqual(result[1].count, 5) class TestTagDelete(TestTags): - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_tag_delete(self, mock_post): """Check that a tag can be deleted""" mock_post.return_value = self._return_value(True) @@ -44,7 +44,7 @@ class TestTagDelete(TestTags): mock_post.assert_called_with("/tag/tag1/delete.json") self.assertEqual(result, True) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_tag_delete_id(self, mock_post): """Check that a tag can be deleted using its ID""" mock_post.return_value = self._return_value(True) @@ -52,14 +52,14 @@ class TestTagDelete(TestTags): mock_post.assert_called_with("/tag/tag1/delete.json") self.assertEqual(result, True) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_tag_delete_failure(self, mock_post): """Check that an exception is raised if a tag cannot be deleted""" mock_post.return_value = self._return_value(False) - with self.assertRaises(openphoto.OpenPhotoError): + with self.assertRaises(trovebox.TroveboxError): self.client.tag.delete(self.test_tags[0]) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_tag_object_delete(self, mock_post): """Check that a tag can be deleted when using the tag object directly""" mock_post.return_value = self._return_value(True) @@ -70,18 +70,18 @@ class TestTagDelete(TestTags): self.assertEqual(tag.get_fields(), {}) self.assertEqual(tag.id, None) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_tag_object_delete_failure(self, mock_post): """ Check that an exception is raised if a tag cannot be deleted when using the tag object directly """ mock_post.return_value = self._return_value(False) - with self.assertRaises(openphoto.OpenPhotoError): + with self.assertRaises(trovebox.TroveboxError): self.test_tags[0].delete() class TestTagUpdate(TestTags): - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_tag_update(self, mock_post): """Check that a tag can be updated""" mock_post.return_value = self._return_value(self.test_tags_dict[1]) @@ -90,7 +90,7 @@ class TestTagUpdate(TestTags): self.assertEqual(result.id, "tag2") self.assertEqual(result.count, 5) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_tag_update_id(self, mock_post): """Check that a tag can be updated using its ID""" mock_post.return_value = self._return_value(self.test_tags_dict[1]) @@ -99,7 +99,7 @@ class TestTagUpdate(TestTags): self.assertEqual(result.id, "tag2") self.assertEqual(result.count, 5) - @mock.patch.object(openphoto.OpenPhoto, 'post') + @mock.patch.object(trovebox.Trovebox, 'post') def test_tag_object_update(self, mock_post): """Check that a tag can be updated when using the tag object directly""" mock_post.return_value = self._return_value(self.test_tags_dict[1]) diff --git a/trovebox/__init__.py b/trovebox/__init__.py new file mode 100644 index 0000000..c1118e2 --- /dev/null +++ b/trovebox/__init__.py @@ -0,0 +1,35 @@ +from .http import Http +from .errors import * +from ._version import __version__ +from . import api_photo +from . import api_tag +from . import api_album + +LATEST_API_VERSION = 2 + +class Trovebox(Http): + """ + Client library for Trovebox + If no parameters are specified, config is loaded from the default + location (~/.config/trovebox/default). + The config_file parameter is used to specify an alternate config file. + If the host parameter is specified, no config file is loaded and + OAuth tokens (consumer*, token*) can optionally be specified. + All requests will include the api_version path, if specified. + This should be used to ensure that your application will continue to work + even if the Trovebox API is updated to a new revision. + """ + def __init__(self, config_file=None, host=None, + consumer_key='', consumer_secret='', + token='', token_secret='', + api_version=None): + Http.__init__(self, config_file, host, + consumer_key, consumer_secret, + token, token_secret, api_version) + + self.photos = api_photo.ApiPhotos(self) + self.photo = api_photo.ApiPhoto(self) + self.tags = api_tag.ApiTags(self) + self.tag = api_tag.ApiTag(self) + self.albums = api_album.ApiAlbums(self) + self.album = api_album.ApiAlbum(self) diff --git a/openphoto/_version.py b/trovebox/_version.py similarity index 100% rename from openphoto/_version.py rename to trovebox/_version.py diff --git a/openphoto/api_album.py b/trovebox/api_album.py similarity index 95% rename from openphoto/api_album.py rename to trovebox/api_album.py index f83561f..9c212c0 100644 --- a/openphoto/api_album.py +++ b/trovebox/api_album.py @@ -1,4 +1,4 @@ -from openphoto.objects import Album +from .objects import Album class ApiAlbums: def __init__(self, client): @@ -23,7 +23,7 @@ class ApiAlbum: """ Delete an album. Returns True if successful. - Raises an OpenPhotoError if not. + Raises an TroveboxError if not. """ if not isinstance(album, Album): album = Album(self._client, {"id": album}) diff --git a/openphoto/api_photo.py b/trovebox/api_photo.py similarity index 90% rename from openphoto/api_photo.py rename to trovebox/api_photo.py index c5052ef..6c1eacf 100644 --- a/openphoto/api_photo.py +++ b/trovebox/api_photo.py @@ -1,8 +1,8 @@ import base64 -from openphoto.errors import OpenPhotoError -import openphoto.openphoto_http -from openphoto.objects import Photo +from .errors import TroveboxError +from . import http +from .objects import Photo def extract_ids(photos): """ @@ -24,31 +24,31 @@ class ApiPhotos: def list(self, **kwds): """ Returns a list of Photo objects """ photos = self._client.get("/photos/list.json", **kwds)["result"] - photos = openphoto.openphoto_http.result_to_list(photos) + photos = http.result_to_list(photos) return [Photo(self._client, photo) for photo in photos] def update(self, photos, **kwds): """ Updates a list of photos. Returns True if successful. - Raises OpenPhotoError if not. + Raises TroveboxError if not. """ ids = extract_ids(photos) if not self._client.post("/photos/update.json", ids=ids, **kwds)["result"]: - raise OpenPhotoError("Update response returned False") + raise TroveboxError("Update response returned False") return True def delete(self, photos, **kwds): """ Deletes a list of photos. Returns True if successful. - Raises OpenPhotoError if not. + Raises TroveboxError if not. """ ids = extract_ids(photos) if not self._client.post("/photos/delete.json", ids=ids, **kwds)["result"]: - raise OpenPhotoError("Delete response returned False") + raise TroveboxError("Delete response returned False") return True class ApiPhoto: @@ -59,7 +59,7 @@ class ApiPhoto: """ Delete a photo. Returns True if successful. - Raises an OpenPhotoError if not. + Raises an TroveboxError if not. """ if not isinstance(photo, Photo): photo = Photo(self._client, {"id": photo}) diff --git a/openphoto/api_tag.py b/trovebox/api_tag.py similarity index 93% rename from openphoto/api_tag.py rename to trovebox/api_tag.py index 63beea1..4221c9a 100644 --- a/openphoto/api_tag.py +++ b/trovebox/api_tag.py @@ -1,4 +1,4 @@ -from openphoto.objects import Tag +from .objects import Tag class ApiTags: def __init__(self, client): @@ -24,7 +24,7 @@ class ApiTag: """ Delete a tag. Returns True if successful. - Raises an OpenPhotoError if not. + Raises an TroveboxError if not. """ if not isinstance(tag, Tag): tag = Tag(self._client, {"id": tag}) diff --git a/openphoto/config.py b/trovebox/config.py similarity index 97% rename from openphoto/config.py rename to trovebox/config.py index 2589f13..bacef2b 100644 --- a/openphoto/config.py +++ b/trovebox/config.py @@ -41,7 +41,7 @@ def get_config_path(config_file): config_path = os.path.join(os.getenv('HOME'), ".config") if not config_file: config_file = "default" - return os.path.join(config_path, "openphoto", config_file) + return os.path.join(config_path, "trovebox", config_file) def read_config(config_path): """ diff --git a/openphoto/errors.py b/trovebox/errors.py similarity index 54% rename from openphoto/errors.py rename to trovebox/errors.py index 2c22177..c813e5a 100644 --- a/openphoto/errors.py +++ b/trovebox/errors.py @@ -1,12 +1,12 @@ -class OpenPhotoError(Exception): - """ Indicates that an OpenPhoto operation failed """ +class TroveboxError(Exception): + """ Indicates that an Trovebox operation failed """ pass -class OpenPhotoDuplicateError(OpenPhotoError): +class TroveboxDuplicateError(TroveboxError): """ Indicates that an upload operation failed due to a duplicate photo """ pass -class OpenPhoto404Error(Exception): +class Trovebox404Error(Exception): """ Indicates that an Http 404 error code was received (resource not found) diff --git a/openphoto/openphoto_http.py b/trovebox/http.py similarity index 86% rename from openphoto/openphoto_http.py rename to trovebox/http.py index d1e2475..fef1fdc 100644 --- a/openphoto/openphoto_http.py +++ b/trovebox/http.py @@ -8,9 +8,9 @@ try: except ImportError: from urlparse import urlunparse # Python2 -from openphoto.objects import OpenPhotoObject -from openphoto.errors import * -from openphoto.config import Config +from .objects import TroveboxObject +from .errors import * +from .config import Config if sys.version < '3': TEXT_TYPE = unicode @@ -20,24 +20,24 @@ else: DUPLICATE_RESPONSE = {"code": 409, "message": "This photo already exists"} -class OpenPhotoHttp: +class Http: """ - Base class to handle HTTP requests to an OpenPhoto server. + Base class to handle HTTP requests to an Trovebox server. If no parameters are specified, config is loaded from the default - location (~/.config/openphoto/default). + location (~/.config/trovebox/default). The config_file parameter is used to specify an alternate config file. If the host parameter is specified, no config file is loaded and OAuth tokens (consumer*, token*) can optionally be specified. All requests will include the api_version path, if specified. This should be used to ensure that your application will continue to work - even if the OpenPhoto API is updated to a new revision. + even if the Trovebox API is updated to a new revision. """ def __init__(self, config_file=None, host=None, consumer_key='', consumer_secret='', token='', token_secret='', api_version=None): self._api_version = api_version - self._logger = logging.getLogger("openphoto") + self._logger = logging.getLogger("trovebox") self.config = Config(config_file, host, consumer_key, consumer_secret, @@ -55,7 +55,7 @@ class OpenPhotoHttp: Performs an HTTP GET from the specified endpoint (API path), passing parameters if given. The api_version is prepended to the endpoint, - if it was specified when the OpenPhoto object was created. + if it was specified when the Trovebox object was created. Returns the decoded JSON dictionary, and raises exceptions if an error code is received. @@ -98,7 +98,7 @@ class OpenPhotoHttp: Performs an HTTP POST to the specified endpoint (API path), passing parameters if given. The api_version is prepended to the endpoint, - if it was specified when the OpenPhoto object was created. + if it was specified when the Trovebox object was created. Returns the decoded JSON dictionary, and raises exceptions if an error code is received. @@ -112,7 +112,7 @@ class OpenPhotoHttp: url = urlunparse(('http', self.host, endpoint, '', '', '')) if not self.config.consumer_key: - raise OpenPhotoError("Cannot issue POST without OAuth tokens") + raise TroveboxError("Cannot issue POST without OAuth tokens") auth = requests_oauthlib.OAuth1(self.config.consumer_key, self.config.consumer_secret, @@ -152,7 +152,7 @@ class OpenPhotoHttp: processed_params = {} for key, value in params.items(): # Extract IDs from objects - if isinstance(value, OpenPhotoObject): + if isinstance(value, TroveboxObject): value = value.id # Ensure value is UTF-8 encoded @@ -165,7 +165,7 @@ class OpenPhotoHttp: new_list = list(value) # Extract IDs from objects in the list for i, item in enumerate(new_list): - if isinstance(item, OpenPhotoObject): + if isinstance(item, TroveboxObject): new_list[i] = item.id # Convert list to string value = ','.join([str(item) for item in new_list]) @@ -184,28 +184,28 @@ class OpenPhotoHttp: Raises an exception if an invalid response code is received. """ if response.status_code == 404: - raise OpenPhoto404Error("HTTP Error %d: %s" % - (response.status_code, response.reason)) + raise Trovebox404Error("HTTP Error %d: %s" % + (response.status_code, response.reason)) try: json_response = response.json() code = json_response["code"] message = json_response["message"] except (ValueError, KeyError): - # Response wasn't OpenPhoto JSON - check the HTTP status code + # Response wasn't Trovebox JSON - check the HTTP status code if 200 <= response.status_code < 300: # Status code was valid, so just reraise the exception raise else: - raise OpenPhotoError("HTTP Error %d: %s" % - (response.status_code, response.reason)) + raise TroveboxError("HTTP Error %d: %s" % + (response.status_code, response.reason)) if 200 <= code < 300: return json_response elif (code == DUPLICATE_RESPONSE["code"] and DUPLICATE_RESPONSE["message"] in message): - raise OpenPhotoDuplicateError("Code %d: %s" % (code, message)) + raise TroveboxDuplicateError("Code %d: %s" % (code, message)) else: - raise OpenPhotoError("Code %d: %s" % (code, message)) + raise TroveboxError("Code %d: %s" % (code, message)) def result_to_list(result): """ Handle the case where the result contains no items """ diff --git a/openphoto/main.py b/trovebox/main.py similarity index 93% rename from openphoto/main.py rename to trovebox/main.py index 4aacbc0..dfe2573 100644 --- a/openphoto/main.py +++ b/trovebox/main.py @@ -4,7 +4,7 @@ import sys import json from optparse import OptionParser -import openphoto +import trovebox CONFIG_ERROR = """ You must create a configuration file with the following contents: @@ -29,7 +29,7 @@ def main(args=sys.argv[1:]): parser.add_option('-c', '--config', help="Configuration file to use", action='store', type='string', dest='config_file') parser.add_option('-h', '-H', '--host', - help=("Hostname of the OpenPhoto server " + help=("Hostname of the Trovebox server " "(overrides config_file)"), action='store', type='string', dest='host') parser.add_option('-X', help="Method to use (GET or POST)", @@ -56,7 +56,7 @@ def main(args=sys.argv[1:]): return if options.version: - print(openphoto.__version__) + print(trovebox.__version__) return if args: @@ -70,10 +70,10 @@ def main(args=sys.argv[1:]): # Host option overrides config file settings if options.host: - client = openphoto.OpenPhoto(host=options.host) + client = trovebox.Trovebox(host=options.host) else: try: - client = openphoto.OpenPhoto(config_file=options.config_file) + client = trovebox.Trovebox(config_file=options.config_file) except IOError as error: print(error) print(CONFIG_ERROR) @@ -108,7 +108,7 @@ def main(args=sys.argv[1:]): def extract_files(params): """ Extract filenames from the "photo" parameter, so they can be uploaded, returning (updated_params, files). - Uses the same technique as openphoto-php: + Uses the same technique as the Trovebox PHP commandline tool: * Filename can only be in the "photo" parameter * Filename must be prefixed with "@" * Filename must exist diff --git a/openphoto/objects.py b/trovebox/objects.py similarity index 71% rename from openphoto/objects.py rename to trovebox/objects.py index 8072188..ffced54 100644 --- a/openphoto/objects.py +++ b/trovebox/objects.py @@ -3,14 +3,14 @@ try: except ImportError: from urllib import quote # Python2 -from openphoto.errors import OpenPhotoError +from .errors import TroveboxError -class OpenPhotoObject: +class TroveboxObject: """ Base object supporting the storage of custom fields as attributes """ - def __init__(self, openphoto, json_dict): + def __init__(self, trovebox, json_dict): self.id = None self.name = None - self._openphoto = openphoto + self._trovebox = trovebox self._json_dict = json_dict self._set_fields(json_dict) @@ -54,24 +54,24 @@ class OpenPhotoObject: return self._json_dict -class Photo(OpenPhotoObject): +class Photo(TroveboxObject): def delete(self, **kwds): """ Delete this photo. Returns True if successful. - Raises an OpenPhotoError if not. + Raises an TroveboxError if not. """ - result = self._openphoto.post("/photo/%s/delete.json" % - self.id, **kwds)["result"] + result = self._trovebox.post("/photo/%s/delete.json" % + self.id, **kwds)["result"] if not result: - raise OpenPhotoError("Delete response returned False") + raise TroveboxError("Delete response returned False") self._delete_fields() return result def edit(self, **kwds): """ Returns an HTML form to edit the photo """ - result = self._openphoto.get("/photo/%s/edit.json" % - self.id, **kwds)["result"] + result = self._trovebox.get("/photo/%s/edit.json" % + self.id, **kwds)["result"] return result["markup"] def replace(self, photo_file, **kwds): @@ -82,8 +82,8 @@ class Photo(OpenPhotoObject): def update(self, **kwds): """ Update this photo with the specified parameters """ - new_dict = self._openphoto.post("/photo/%s/update.json" % - self.id, **kwds)["result"] + new_dict = self._trovebox.post("/photo/%s/update.json" % + self.id, **kwds)["result"] self._replace_fields(new_dict) def view(self, **kwds): @@ -91,8 +91,8 @@ class Photo(OpenPhotoObject): Used to view the photo at a particular size. Updates the photo's fields with the response. """ - new_dict = self._openphoto.get("/photo/%s/view.json" % - self.id, **kwds)["result"] + new_dict = self._trovebox.get("/photo/%s/view.json" % + self.id, **kwds)["result"] self._replace_fields(new_dict) def dynamic_url(self, **kwds): @@ -103,7 +103,7 @@ class Photo(OpenPhotoObject): Returns a dict containing the next and previous photo lists (there may be more than one next/previous photo returned). """ - result = self._openphoto.get("/photo/%s/nextprevious.json" % + result = self._trovebox.get("/photo/%s/nextprevious.json" % self.id, **kwds)["result"] value = {} if "next" in result: @@ -113,7 +113,7 @@ class Photo(OpenPhotoObject): value["next"] = [] for photo in result["next"]: - value["next"].append(Photo(self._openphoto, photo)) + value["next"].append(Photo(self._trovebox, photo)) if "previous" in result: # Workaround for APIv1 @@ -122,7 +122,7 @@ class Photo(OpenPhotoObject): value["previous"] = [] for photo in result["previous"]: - value["previous"].append(Photo(self._openphoto, photo)) + value["previous"].append(Photo(self._trovebox, photo)) return value @@ -131,65 +131,65 @@ class Photo(OpenPhotoObject): Performs transformation specified in **kwds Example: transform(rotate=90) """ - new_dict = self._openphoto.post("/photo/%s/transform.json" % - self.id, **kwds)["result"] + new_dict = self._trovebox.post("/photo/%s/transform.json" % + self.id, **kwds)["result"] # APIv1 doesn't return the transformed photo (frontend issue #955) if isinstance(new_dict, bool): - new_dict = self._openphoto.get("/photo/%s/view.json" % - self.id)["result"] + new_dict = self._trovebox.get("/photo/%s/view.json" % + self.id)["result"] self._replace_fields(new_dict) -class Tag(OpenPhotoObject): +class Tag(TroveboxObject): def delete(self, **kwds): """ Delete this tag. Returns True if successful. - Raises an OpenPhotoError if not. + Raises an TroveboxError if not. """ - result = self._openphoto.post("/tag/%s/delete.json" % - quote(self.id), **kwds)["result"] + result = self._trovebox.post("/tag/%s/delete.json" % + quote(self.id), **kwds)["result"] if not result: - raise OpenPhotoError("Delete response returned False") + raise TroveboxError("Delete response returned False") self._delete_fields() return result def update(self, **kwds): """ Update this tag with the specified parameters """ - new_dict = self._openphoto.post("/tag/%s/update.json" % quote(self.id), - **kwds)["result"] + new_dict = self._trovebox.post("/tag/%s/update.json" % quote(self.id), + **kwds)["result"] self._replace_fields(new_dict) -class Album(OpenPhotoObject): - def __init__(self, openphoto, json_dict): +class Album(TroveboxObject): + def __init__(self, trovebox, json_dict): self.photos = None self.cover = None - OpenPhotoObject.__init__(self, openphoto, json_dict) + TroveboxObject.__init__(self, trovebox, json_dict) self._update_fields_with_objects() def _update_fields_with_objects(self): """ Convert dict fields into objects, where appropriate """ # Update the cover with a photo object if isinstance(self.cover, dict): - self.cover = Photo(self._openphoto, self.cover) + self.cover = Photo(self._trovebox, self.cover) # Update the photo list with photo objects if isinstance(self.photos, list): for i, photo in enumerate(self.photos): if isinstance(photo, dict): - self.photos[i] = Photo(self._openphoto, photo) + self.photos[i] = Photo(self._trovebox, photo) def delete(self, **kwds): """ Delete this album. Returns True if successful. - Raises an OpenPhotoError if not. + Raises an TroveboxError if not. """ - result = self._openphoto.post("/album/%s/delete.json" % - self.id, **kwds)["result"] + result = self._trovebox.post("/album/%s/delete.json" % + self.id, **kwds)["result"] if not result: - raise OpenPhotoError("Delete response returned False") + raise TroveboxError("Delete response returned False") self._delete_fields() return result @@ -204,12 +204,12 @@ class Album(OpenPhotoObject): def update(self, **kwds): """ Update this album with the specified parameters """ - new_dict = self._openphoto.post("/album/%s/update.json" % - self.id, **kwds)["result"] + new_dict = self._trovebox.post("/album/%s/update.json" % + self.id, **kwds)["result"] # APIv1 doesn't return the updated album (frontend issue #937) if isinstance(new_dict, bool): - new_dict = self._openphoto.get("/album/%s/view.json" % + new_dict = self._trovebox.get("/album/%s/view.json" % self.id)["result"] self._replace_fields(new_dict) @@ -220,7 +220,7 @@ class Album(OpenPhotoObject): Requests the full contents of the album. Updates the album's fields with the response. """ - result = self._openphoto.get("/album/%s/view.json" % - self.id, **kwds)["result"] + result = self._trovebox.get("/album/%s/view.json" % + self.id, **kwds)["result"] self._replace_fields(result) self._update_fields_with_objects() From 0c61b39e7def578228f3bf109193df05fe8fad21 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Fri, 19 Jul 2013 17:54:05 +0100 Subject: [PATCH 56/58] Additional openphoto->trovebox renames --- README.rst | 8 ++++---- setup.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 5f8f24b..4b1ef5e 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ -================================= -OpenPhoto/Trovebox Python Library -================================= +======================= +Trovebox Python Library +======================= .. image:: https://api.travis-ci.org/photo/openphoto-python.png :alt: Build Status :target: https://travis-ci.org/photo/openphoto-python @@ -115,7 +115,7 @@ Upload a public photo to the host specified in ```~/.config/trovebox/default```: Get a thumbnail URL from current.trovebox.com (unauthenticated access):: - openphoto -h current.trovebox.com -p -e /photo/62/view.json -F 'returnSizes=20x20' + trovebox -h current.trovebox.com -p -e /photo/62/view.json -F 'returnSizes=20x20' { "code":200, "message":"Photo 62", diff --git a/setup.py b/setup.py index 4f71e1c..034d3c9 100755 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ setup(name='trovebox', packages=['trovebox'], data_files=['README.rst'], keywords=['openphoto', 'pyopenphoto', 'openphoto-python', - 'trovebox', 'pytrovebox', 'openphoto-trovebox'], + 'trovebox', 'pytrovebox', 'trovebox-python'], classifiers=['Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', From 8249e78e1d19ac5bb80c77dacf98dc5c982ce185 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Fri, 19 Jul 2013 17:56:42 +0100 Subject: [PATCH 57/58] Fixes to README --- README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 4b1ef5e..86e9742 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,8 @@ ======================= Trovebox Python Library ======================= +(Previously known as openphoto-python) + .. image:: https://api.travis-ci.org/photo/openphoto-python.png :alt: Build Status :target: https://travis-ci.org/photo/openphoto-python @@ -11,7 +13,7 @@ Trovebox Python Library This library works with any Trovebox server, either `Self-hosted `__, or using the -`Trovebox hosted service`__). +`Trovebox hosted service `__). It provides full access to your photos and metadata, via a simple Pythonic API. From 345e7c5900997c40f6a7c56430363f1b8a00ecd3 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 20 Jul 2013 14:04:24 +0100 Subject: [PATCH 58/58] Minor README fix --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 86e9742..e66dba1 100644 --- a/README.rst +++ b/README.rst @@ -12,8 +12,8 @@ Trovebox Python Library :target: https://pypi.python.org/pypi/trovebox This library works with any Trovebox server, either -`Self-hosted `__, or using the -`Trovebox hosted service `__). +`self-hosted `__, or using the hosted service at +`trovebox.com `__. It provides full access to your photos and metadata, via a simple Pythonic API.