From c627f47cf515fa522b5f07cdf1bddef5fe0ee049 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Wed, 30 Nov 2016 14:19:47 +0100 Subject: [PATCH 1/5] pep8 refactoring in tests Didn't want all highlights in sublime text... --- tests/test_errors.py | 39 ++++++--- tests/test_pycrest.py | 178 +++++++++++++++++++++++++++--------------- 2 files changed, 142 insertions(+), 75 deletions(-) diff --git a/tests/test_errors.py b/tests/test_errors.py index 9c5e2e1..5a22aa4 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,6 +1,8 @@ -from pycrest.errors import APIException, UnsupportedHTTPMethodException import unittest +from pycrest.errors import APIException +from pycrest.errors import UnsupportedHTTPMethodException + try: import __builtin__ builtins_name = __builtin__.__name__ @@ -9,14 +11,17 @@ builtins_name = builtins.__name__ - class TestAPIException(unittest.TestCase): def setUp(self): pass def test_apiexception_data(self): - e = APIException('http://example.com', 205, {'message' : 'example error'}) + e = APIException( + 'http://example.com', + 205, + {'message': 'example error'} + ) self.assertEqual( e.url, @@ -27,28 +32,38 @@ def test_apiexception_data(self): 205) def test_apiexception_str_message(self): - e = APIException('http://example.com', 205, {'message' : 'example error'}) + e = APIException( + 'http://example.com', + 205, + {'message': 'example error'} + ) self.assertIn( 'example error', str(e)) - self.assertIn( '205', str(e) ) + self.assertIn('205', str(e)) def test_apiexception_str_error(self): - e = APIException('http://example.com', 205, {'error' : 'example error'}) + e = APIException( + 'http://example.com', + 205, + {'error': 'example error'} + ) self.assertIn( 'example error', str(e)) - self.assertIn( '205', str(e) ) - - + self.assertIn('205', str(e)) def test_apiexception_str_no_message(self): - e = APIException('http://example.com', 205, {'exception_type' : 'wierd'}) - self.assertIn( '205', str(e) ) + e = APIException( + 'http://example.com', + 205, + {'exception_type': 'wierd'} + ) + self.assertIn('205', str(e)) class TestUnsupportedHTTPMethodException(unittest.TestCase): @@ -57,7 +72,7 @@ def setUp(self): def test_exception_str(self): e = UnsupportedHTTPMethodException('flatten') - self.assertIn( 'flatten', str(e) ) + self.assertIn('flatten', str(e)) if __name__ == "__main__": diff --git a/tests/test_pycrest.py b/tests/test_pycrest.py index 97b7942..acdfe8e 100644 --- a/tests/test_pycrest.py +++ b/tests/test_pycrest.py @@ -3,19 +3,25 @@ @author: henk ''' -import sys -from pycrest.eve import EVE, APIObject -from pycrest.cache import DictCache, DummyCache, APICache, FileCache,\ - MemcachedCache +import errno import httmock -import pycrest import mock -import errno -from pycrest.errors import APIException, UnsupportedHTTPMethodException -from requests.models import PreparedRequest -from requests.adapters import HTTPAdapter +import pycrest +import sys import unittest +from pycrest.cache import APICache +from pycrest.cache import DictCache +from pycrest.cache import DummyCache +from pycrest.cache import FileCache +from pycrest.cache import MemcachedCache +from pycrest.errors import APIException +from pycrest.errors import UnsupportedHTTPMethodException +from pycrest.eve import APIObject +from pycrest.eve import EVE +from requests.adapters import HTTPAdapter +from requests.models import PreparedRequest + try: import __builtin__ builtins_name = __builtin__.__name__ @@ -106,6 +112,7 @@ def market_prices_mock(url, request): ' "pageCount_str": "1",' ' "totalCount": 10213}') + @httmock.urlmatch( scheme="https", netloc=r"(api-sisi\.test)?(crest-tq\.)?eveonline\.com$", @@ -263,63 +270,79 @@ def check_custom_headers(url, request): EVE(additional_headers={'PyCrest-Testing': True}) def test_custom_transport_adapter(self): - """ Check if the transport adapter is the one expected (especially if we set it) """ + """ Check if the transport adapter is the one expected + (especially if we set it) + """ class TestHttpAdapter(HTTPAdapter): def __init__(self, *args, **kwargs): super(TestHttpAdapter, self).__init__(*args, **kwargs) - + class FakeHttpAdapter(object): def __init__(self, *args, **kwargs): pass - + eve = EVE() - self.assertTrue(isinstance(eve._session.get_adapter('http://'), HTTPAdapter)) - self.assertTrue(isinstance(eve._session.get_adapter('https://'), HTTPAdapter)) - self.assertFalse(isinstance(eve._session.get_adapter('http://'), TestHttpAdapter)) - self.assertFalse(isinstance(eve._session.get_adapter('https://'), TestHttpAdapter)) - + self.assertTrue( + isinstance(eve._session.get_adapter('http://'), HTTPAdapter) + ) + self.assertTrue( + isinstance(eve._session.get_adapter('https://'), HTTPAdapter) + ) + self.assertFalse( + isinstance(eve._session.get_adapter('http://'), TestHttpAdapter) + ) + self.assertFalse( + isinstance(eve._session.get_adapter('https://'), TestHttpAdapter) + ) + eve = EVE(transport_adapter=TestHttpAdapter()) - self.assertTrue(isinstance(eve._session.get_adapter('http://'), TestHttpAdapter)) - self.assertTrue(isinstance(eve._session.get_adapter('https://'), TestHttpAdapter)) - + self.assertTrue( + isinstance(eve._session.get_adapter('http://'), TestHttpAdapter) + ) + self.assertTrue( + isinstance(eve._session.get_adapter('https://'), TestHttpAdapter) + ) + # check that the wrong httpadapter is not used eve = EVE(transport_adapter=FakeHttpAdapter()) - self.assertTrue(isinstance(eve._session.get_adapter('http://'), HTTPAdapter)) - self.assertFalse(isinstance(eve._session.get_adapter('http://'), FakeHttpAdapter)) - + self.assertTrue( + isinstance(eve._session.get_adapter('http://'), HTTPAdapter) + ) + self.assertFalse( + isinstance(eve._session.get_adapter('http://'), FakeHttpAdapter) + ) + eve = EVE(transport_adapter='') - self.assertTrue(isinstance(eve._session.get_adapter('http://'), HTTPAdapter)) - - + self.assertTrue( + isinstance(eve._session.get_adapter('http://'), HTTPAdapter) + ) + def test_default_cache(self): self.assertTrue(isinstance(self.api.cache, DictCache)) def test_no_cache(self): eve = EVE(cache=None) self.assertTrue(isinstance(eve.cache, DummyCache)) - + def test_implements_apiobject(self): class CustomCache(object): pass with self.assertRaises(ValueError): - eve = EVE(cache=CustomCache) + EVE(cache=CustomCache) def test_apicache(self): eve = EVE(cache=DictCache()) self.assertTrue(isinstance(eve.cache, DictCache)) - - @mock.patch('os.path.isdir', return_value=False) - @mock.patch('os.mkdir') + @mock.patch('os.path.isdir', return_value=False) + @mock.patch('os.mkdir') def test_file_cache(self, mkdir_function, isdir_function): file_cache = FileCache(path=TestFileCache.DIR) - eve = EVE(cache=file_cache) - self.assertEqual(file_cache.path, TestFileCache.DIR) - self.assertTrue(isinstance(eve.cache, FileCache)) - + eve = EVE(cache=file_cache) + self.assertEqual(file_cache.path, TestFileCache.DIR) + self.assertTrue(isinstance(eve.cache, FileCache)) def test_default_url(self): - @httmock.all_requests def root_mock(url, request): self.assertEqual(url.path, '/') @@ -331,7 +354,6 @@ def root_mock(url, request): self.api() def test_parse_parameters_url(self): - @httmock.all_requests def key_mock(url, request): self.assertEqual(url.path, '/') @@ -343,7 +365,6 @@ def key_mock(url, request): self.api.get('https://crest-tq.eveonline.com/?key=value1') def test_parse_parameters_override(self): - @httmock.all_requests def key_mock(url, request): self.assertEqual(url.path, '/') @@ -374,39 +395,42 @@ def cached_request(url, request): with httmock.HTTMock(cached_request): self.api._data = None self.assertEqual(self.api()._dict, {}) - + def test_caching_arg_hit(self): - """ Test the caching argument for ApiConnection and ApiObject __call__() """ - + """ Test the caching argument for ApiConnection + and ApiObject __call__() + """ + @httmock.urlmatch( scheme="https", netloc=r"(api-sisi\.test)?(crest-tq\.)?eveonline\.com$", path=r"^/market/prices/?$") def market_prices_cached_mock(url, request): - headers = {'content-type': 'application/json', - 'Cache-Control': 'max-age=300;'} + headers = { + 'content-type': 'application/json', + 'Cache-Control': 'max-age=300;' + } return httmock.response( status_code=200, headers=headers, content='{}'.encode('utf-8')) - + with httmock.HTTMock(root_mock, market_prices_cached_mock): self.assertEqual(self.api.cache._dict, {}) - + self.api(caching=False) self.assertEqual(self.api.cache._dict, {}) - + self.api._data = None self.api() self.assertEqual(len(self.api.cache._dict), 1) - + self.assertEqual(self.api().marketData(caching=False)._dict, {}) self.assertEqual(len(self.api.cache._dict), 1) - + self.assertEqual(self.api().marketData()._dict, {}) self.assertEqual(len(self.api.cache._dict), 2) - def test_cache_invalidate(self): @httmock.all_requests def prime_cache(url, request): @@ -436,7 +460,7 @@ def test_non_http_200(self): @httmock.all_requests def non_http_200(url, request): - return {'status_code': 404, 'content' : {'message' : 'not found'}} + return {'status_code': 404, 'content': {'message': 'not found'}} with httmock.HTTMock(non_http_200): self.assertRaises(APIException, self.api) @@ -516,7 +540,8 @@ def test_invalidate(self): def test_cache_dir(self): pass - + + class TestDummyCache(unittest.TestCase): def setUp(self): @@ -688,40 +713,60 @@ def test_non_http_200_201_post(self): @httmock.all_requests def non_http_200(url, request): - return {'status_code': 404, 'content' : {'message' : 'not found'}} + return {'status_code': 404, 'content': {'message': 'not found'}} with httmock.HTTMock(non_http_200): - self.assertRaises(APIException, self.api.writeableEndpoint, method='post') + self.assertRaises( + APIException, + self.api.writeableEndpoint, + method='post' + ) def test_non_http_200_put(self): @httmock.all_requests def non_http_200(url, request): - return {'status_code': 201, 'content' : {'message' : 'created new object'}} + return { + 'status_code': 201, + 'content': {'message': 'created new object'} + } with httmock.HTTMock(non_http_200): - self.assertRaises(APIException, self.api.writeableEndpoint, method='put') + self.assertRaises( + APIException, + self.api.writeableEndpoint, + method='put' + ) def test_non_http_200_delete(self): @httmock.all_requests def non_http_200(url, request): - return {'status_code': 201, 'content' : {'message' : 'created new object'}} + return { + 'status_code': 201, + 'content': {'message': 'created new object'} + } with httmock.HTTMock(non_http_200): - self.assertRaises(APIException, self.api.writeableEndpoint, method='delete') + self.assertRaises( + APIException, + self.api.writeableEndpoint, + method='delete' + ) - #201 received from successful contact creation via POST + # 201 received from successful contact creation via POST def test_http_201_post(self): @httmock.all_requests def http_201(url, request): - return {'status_code': 201, 'content' : {'message' : 'created new object'}} + return { + 'status_code': 201, + 'content': {'message': 'created new object'} + } with httmock.HTTMock(http_201): res = self.api.writeableEndpoint(method='post') self.assertTrue(isinstance(res, APIObject)) - def test_double_call_self(self): with httmock.HTTMock(*all_httmocks): r1 = self.api() @@ -736,19 +781,26 @@ def test_deprecated_parameter_passing(self): def test_string_parameter_passing(self): with httmock.HTTMock(*all_httmocks): - res = self.api.writeableEndpoint(method='post', data='some (json?) data') + res = self.api.writeableEndpoint( + method='post', + data='some (json?) data' + ) self.assertTrue(isinstance(res, APIObject)) def test_dict_parameter_passing(self): with httmock.HTTMock(*all_httmocks): - res = self.api.writeableEndpoint(data={'arg1' : 'val1' }) + res = self.api.writeableEndpoint(data={'arg1': 'val1'}) self.assertTrue(isinstance(res, APIObject)) def test_unhandled_http_method_exception(self): with httmock.HTTMock(*all_httmocks): - self.assertRaises(UnsupportedHTTPMethodException, self.api.writeableEndpoint, method='snip') #made-up http method + self.assertRaises( + UnsupportedHTTPMethodException, + self.api.writeableEndpoint, + method='snip' + ) # made-up http method if __name__ == "__main__": unittest.main() From 2a53712c280a953c9a97d22962e85368ebb4b0e3 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Wed, 30 Nov 2016 14:20:14 +0100 Subject: [PATCH 2/5] update gitignore with some more python ignores --- .gitignore | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 0057cca..14ea9c6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,10 +7,30 @@ locustfile.py *.pyc -*.egg-info/ -*.egg/ .idea/ htmlcov/ +docs/_*/ + +# Distribution / packaging +.Python +env/ build/ +develop-eggs/ dist/ -docs/_*/ \ No newline at end of file +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class \ No newline at end of file From 048f19ecdd639e20341351f1732462b1bd682b62 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Wed, 30 Nov 2016 14:53:59 +0100 Subject: [PATCH 3/5] Move all the cache tests in a single file for readability --- tests/test_cache.py | 176 ++++++++++++++++++++++++++++++++++++++++++ tests/test_pycrest.py | 159 -------------------------------------- 2 files changed, 176 insertions(+), 159 deletions(-) create mode 100644 tests/test_cache.py diff --git a/tests/test_cache.py b/tests/test_cache.py new file mode 100644 index 0000000..059e250 --- /dev/null +++ b/tests/test_cache.py @@ -0,0 +1,176 @@ +import errno +import mock +import sys +import unittest + +from pycrest.cache import APICache +from pycrest.cache import DictCache +from pycrest.cache import DummyCache +from pycrest.cache import FileCache +from pycrest.cache import MemcachedCache + +try: + import __builtin__ + builtins_name = __builtin__.__name__ +except ImportError: + import builtins + builtins_name = builtins.__name__ + + +class TestAPICache(unittest.TestCase): + + def setUp(self): + self.c = APICache() + + def test_put(self): + self.assertRaises(NotImplementedError, self.c.get, 'key') + + def test_get(self): + self.assertRaises(NotImplementedError, self.c.put, 'key', 'val') + + def test_invalidate(self): + self.assertRaises(NotImplementedError, self.c.invalidate, 'key') + + +class TestDictCache(unittest.TestCase): + + def setUp(self): + self.c = DictCache() + self.c.put('key', True) + + def test_put(self): + self.assertEqual(self.c._dict['key'], True) + + def test_get(self): + self.assertEqual(self.c.get('key'), True) + + def test_invalidate(self): + self.c.invalidate('key') + self.assertIsNone(self.c.get('key')) + + def test_cache_dir(self): + pass + + +class TestDummyCache(unittest.TestCase): + + def setUp(self): + self.c = DummyCache() + self.c.put('never_stored', True) + + def test_put(self): + self.assertNotIn('never_stored', self.c._dict) + + def test_get(self): + self.assertEqual(self.c.get('never_stored'), None) + + def test_invalidate(self): + self.c.invalidate('never_stored') + self.assertIsNone(self.c.get('never_stored')) + + +class TestFileCache(unittest.TestCase): + ''' + Class for testing the filecache + + TODO: Debug wth this test is creating an SSL connection + ''' + + DIR = '/tmp/TestFileCache' + + @mock.patch('os.path.isdir') + @mock.patch('os.mkdir') + @mock.patch('{0}.open'.format(builtins_name)) + def setUp(self, open_function, mkdir_function, isdir_function): + self.c = FileCache(TestFileCache.DIR) + self.c.put('key', 'value') + + @mock.patch('os.path.isdir', return_value=False) + @mock.patch('os.mkdir') + def test_init(self, mkdir_function, isdir_function): + c = FileCache(TestFileCache.DIR) + + # Ensure path has been set + self.assertEqual(c.path, TestFileCache.DIR) + + # Ensure we checked if the dir was already there + args, kwargs = isdir_function.call_args + self.assertEqual((TestFileCache.DIR,), args) + + # Ensure we called mkdir with the right args + args, kwargs = mkdir_function.call_args + self.assertEqual((TestFileCache.DIR, 0o700), args) + +# @unittest.skip("https://github.com/pycrest/PyCrest/issues/30") +# def test_getpath(self): +# self.assertEqual(self.c._getpath('key'), +# os.path.join(TestFileCache.DIR, +# '1140801208126482496.cache')) + + def test_get_uncached(self): + # Check non-existant key + self.assertIsNone(self.c.get('nope')) + + @mock.patch('builtins.open') + def test_get_cached(self, open_function): + self.assertEqual(self.c.get('key'), 'value') + + @unittest.skipIf( + sys.version_info < ( + 3,), 'Python 2.x uses a diffrent protocol') + @mock.patch('{0}.open'.format(builtins_name), mock.mock_open( + read_data=b'x\x9ck`\ne-K\xcc)M-d\xd0\x03\x00\x17\xde\x03\x99')) + def test_get_cached_file_py3(self): + del(self.c._cache['key']) + self.assertEqual(self.c.get('key'), 'value') + + @unittest.skipIf( + sys.version_info > ( + 3,), 'Python 3.x uses a diffrent protocol') + @mock.patch('{0}.open'.format(builtins_name), mock.mock_open( + read_data='x\x9ck`\ne-K\xcc)M-d\xd0\x03\x00\x17\xde\x03\x99')) + def test_get_cached_file_py2(self): + del(self.c._cache['key']) + self.assertEqual(self.c.get('key'), 'value') + + @mock.patch('os.unlink') + def test_invalidate(self, unlink_function): + # Make sure our key is here in the first place + self.assertIn('key', self.c._cache) + + # Unset the key and ensure unlink() was called + self.c.invalidate('key') + self.assertTrue(unlink_function.called) + # TODO: When paths are predictable check the args + # See https://github.com/pycrest/PyCrest/issues/30 + + @mock.patch( + 'os.unlink', + side_effect=OSError( + errno.ENOENT, + 'No such file or directory')) + def test_unlink_exception(self, unlink_function): + self.assertIsNone(self.c.invalidate('key')) + + +class TestMemcachedCache(unittest.TestCase): + '''A very basic MemcachedCache TestCase + + Primairy goal of this unittest is to get the coverage up + to spec. Should probably make use of `mockcache` in the future''' + + memcache_mock = mock.MagicMock() + memcache_mock.get.return_value = 'value' + + @mock.patch('memcache.Client', return_value=memcache_mock) + def setUp(self, mock_memcache): + self.c = MemcachedCache(['127.0.0.1:11211']) + + def test_put(self): + self.c.put('key', 'value') + + def test_get(self): + self.assertEqual(self.c.get('key'), 'value') + + def test_invalidate(self): + self.c.invalidate('key') diff --git a/tests/test_pycrest.py b/tests/test_pycrest.py index acdfe8e..0ea6c71 100644 --- a/tests/test_pycrest.py +++ b/tests/test_pycrest.py @@ -507,165 +507,6 @@ def expired_request(url, request): self.api() -class TestAPICache(unittest.TestCase): - - def setUp(self): - self.c = APICache() - - def test_put(self): - self.assertRaises(NotImplementedError, self.c.get, 'key') - - def test_get(self): - self.assertRaises(NotImplementedError, self.c.put, 'key', 'val') - - def test_invalidate(self): - self.assertRaises(NotImplementedError, self.c.invalidate, 'key') - - -class TestDictCache(unittest.TestCase): - - def setUp(self): - self.c = DictCache() - self.c.put('key', True) - - def test_put(self): - self.assertEqual(self.c._dict['key'], True) - - def test_get(self): - self.assertEqual(self.c.get('key'), True) - - def test_invalidate(self): - self.c.invalidate('key') - self.assertIsNone(self.c.get('key')) - - def test_cache_dir(self): - pass - - -class TestDummyCache(unittest.TestCase): - - def setUp(self): - self.c = DummyCache() - self.c.put('never_stored', True) - - def test_put(self): - self.assertNotIn('never_stored', self.c._dict) - - def test_get(self): - self.assertEqual(self.c.get('never_stored'), None) - - def test_invalidate(self): - self.c.invalidate('never_stored') - self.assertIsNone(self.c.get('never_stored')) - - -class TestFileCache(unittest.TestCase): - ''' - Class for testing the filecache - - TODO: Debug wth this test is creating an SSL connection - ''' - - DIR = '/tmp/TestFileCache' - - @mock.patch('os.path.isdir') - @mock.patch('os.mkdir') - @mock.patch('{0}.open'.format(builtins_name)) - def setUp(self, open_function, mkdir_function, isdir_function): - self.c = FileCache(TestFileCache.DIR) - self.c.put('key', 'value') - - @mock.patch('os.path.isdir', return_value=False) - @mock.patch('os.mkdir') - def test_init(self, mkdir_function, isdir_function): - c = FileCache(TestFileCache.DIR) - - # Ensure path has been set - self.assertEqual(c.path, TestFileCache.DIR) - - # Ensure we checked if the dir was already there - args, kwargs = isdir_function.call_args - self.assertEqual((TestFileCache.DIR,), args) - - # Ensure we called mkdir with the right args - args, kwargs = mkdir_function.call_args - self.assertEqual((TestFileCache.DIR, 0o700), args) - -# @unittest.skip("https://github.com/pycrest/PyCrest/issues/30") -# def test_getpath(self): -# self.assertEqual(self.c._getpath('key'), -# os.path.join(TestFileCache.DIR, -# '1140801208126482496.cache')) - - def test_get_uncached(self): - # Check non-existant key - self.assertIsNone(self.c.get('nope')) - - @mock.patch('builtins.open') - def test_get_cached(self, open_function): - self.assertEqual(self.c.get('key'), 'value') - - @unittest.skipIf( - sys.version_info < ( - 3,), 'Python 2.x uses a diffrent protocol') - @mock.patch('{0}.open'.format(builtins_name), mock.mock_open( - read_data=b'x\x9ck`\ne-K\xcc)M-d\xd0\x03\x00\x17\xde\x03\x99')) - def test_get_cached_file_py3(self): - del(self.c._cache['key']) - self.assertEqual(self.c.get('key'), 'value') - - @unittest.skipIf( - sys.version_info > ( - 3,), 'Python 3.x uses a diffrent protocol') - @mock.patch('{0}.open'.format(builtins_name), mock.mock_open( - read_data='x\x9ck`\ne-K\xcc)M-d\xd0\x03\x00\x17\xde\x03\x99')) - def test_get_cached_file_py2(self): - del(self.c._cache['key']) - self.assertEqual(self.c.get('key'), 'value') - - @mock.patch('os.unlink') - def test_invalidate(self, unlink_function): - # Make sure our key is here in the first place - self.assertIn('key', self.c._cache) - - # Unset the key and ensure unlink() was called - self.c.invalidate('key') - self.assertTrue(unlink_function.called) - # TODO: When paths are predictable check the args - # See https://github.com/pycrest/PyCrest/issues/30 - - @mock.patch( - 'os.unlink', - side_effect=OSError( - errno.ENOENT, - 'No such file or directory')) - def test_unlink_exception(self, unlink_function): - self.assertIsNone(self.c.invalidate('key')) - - -class TestMemcachedCache(unittest.TestCase): - '''A very basic MemcachedCache TestCase - - Primairy goal of this unittest is to get the coverage up - to spec. Should probably make use of `mockcache` in the future''' - - memcache_mock = mock.MagicMock() - memcache_mock.get.return_value = 'value' - - @mock.patch('memcache.Client', return_value=memcache_mock) - def setUp(self, mock_memcache): - self.c = MemcachedCache(['127.0.0.1:11211']) - - def test_put(self): - self.c.put('key', 'value') - - def test_get(self): - self.assertEqual(self.c.get('key'), 'value') - - def test_invalidate(self): - self.c.invalidate('key') - - class TestAPIObject(unittest.TestCase): def setUp(self): From 30f69eee82ceb5cb5cabbf9cedbd8a1ab6309a54 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Wed, 30 Nov 2016 15:20:17 +0100 Subject: [PATCH 4/5] Add a Signal class to manage signals where you need them Also add related tests and doc. + a few pep8 stuff --- docs/index.rst | 39 ++++++++++++++++++++++++++--- pycrest/eve.py | 54 ++++++++++++++++++++++++++++++---------- pycrest/events.py | 58 +++++++++++++++++++++++++++++++++++++++++++ tests/test_events.py | 45 +++++++++++++++++++++++++++++++++ tests/test_pycrest.py | 17 ++++++++----- 5 files changed, 191 insertions(+), 22 deletions(-) create mode 100644 pycrest/events.py create mode 100644 tests/test_events.py diff --git a/docs/index.rst b/docs/index.rst index bf6fbdc..4ebfa25 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -106,7 +106,7 @@ pycrest.cache.DummyCache doesn't cache anything. >>> import pycrest >>> from pycrest.cache import FileCache >>> file_cache = FileCache(path='/tmp/pycrest_cache') ->>> eve = pycrest.EVE(cache=file_cache) +>>> eve = pycrest.EVE(cache=file_cache) .. highlight:: none @@ -190,8 +190,8 @@ This will disable the cache for everything you will do using PyCrest. No call or .. highlight:: none **Disable caching on demand** -You can disable the caching for a specific ``get()`` call you don't want to cache, by simply adding ``caching=False|None`` to the call parameters. -For example: +You can disable the caching for a specific ``get()`` call you don't want to cache, by simply adding ``caching=False|None`` to the call parameters. +For example: .. highlight:: python @@ -200,3 +200,36 @@ For example: >>> regions = crest_root.regions(caching=False) .. highlight:: none + +Signals +------- +Signals are "events" that will be triggered in some circumstances and they can have one or more functions attached as receivers of the event. +These functions will be called automatically when the signal is fired. + +To subscribe to any of these signals, you just need to import it and add your receiver to it. In the same way, you also can remove an receiver from an signal whenever you want, in case you don't want the event to trigger your receiver again. + +.. highlight:: python + +>>> from pycrest.event import after_token_refresh +>>> after_token_refresh.add_receiver(your_receiver_function) # from now, your_receiver_function will be triggered with this signal + +>>> after_token_refresh.remove_receiver(your_receiver_function) # once you do this, your_receiver_function will never be triggered, unless you add it again + +.. highlight:: none + +List of signals +~~~~~~~~~~~~~~~ +after_token_refresh +^^^^^^^^^^^^^^^^^^^ +This signal is triggered as soon as the authorizations are refreshed using the refresh_token. + +List of argument given to the receivers : ++---------------+--------+--------------------------------------+ +| Arguments | Type | Description | ++===============+========+======================================+ +| access_token | String | The new access token used to log in | ++---------------+--------+--------------------------------------+ +| refresh_token | String | The refresh token used to refresh | ++---------------+--------+--------------------------------------+ +| expires | int | The timestamps when the token expires| ++---------------+--------+--------------------------------------+ diff --git a/pycrest/eve.py b/pycrest/eve.py index 0c65b1d..0a2de00 100644 --- a/pycrest/eve.py +++ b/pycrest/eve.py @@ -1,9 +1,13 @@ import base64 import requests import time + from pycrest import version -from pycrest.compat import bytes_, text_ -from pycrest.errors import APIException, UnsupportedHTTPMethodException +from pycrest.compat import bytes_ +from pycrest.compat import text_ +from pycrest.errors import APIException +from pycrest.errors import UnsupportedHTTPMethodException +from pycrest.events import after_token_refresh from requests.adapters import HTTPAdapter try: from urllib.parse import urlparse, urlunparse, parse_qsl @@ -16,7 +20,10 @@ from urllib import quote import logging import re -from pycrest.cache import DictCache, APICache, DummyCache + +from pycrest.cache import APICache +from pycrest.cache import DictCache +from pycrest.cache import DummyCache logger = logging.getLogger("pycrest.eve") cache_re = re.compile(r'max-age=([0-9]+)') @@ -33,9 +40,11 @@ def __init__( '''Initialises a PyCrest object Keyword arguments: - additional_headers - a list of http headers that will be sent to the server + additional_headers - a list of http headers + that will be sent to the server user_agent - a custom user agent - cache - an instance of an APICache object that will cache HTTP Requests. + cache - an instance of an APICache object + that will cache HTTP Requests. Default is DictCache, pass cache=None to disable caching. ''' # Set up a Requests Session @@ -219,7 +228,8 @@ def __getattr__(self, item): def auth_uri(self, scopes=None, state=None): s = [] if not scopes else scopes - return "%s/authorize?response_type=code&redirect_uri=%s&client_id=%s%s%s" % ( + return ("%s/authorize?response_type=code&redirect_uri=%s" + "&client_id=%s%s%s") % ( self._oauth_endpoint, quote(self.redirect_uri, safe=''), self.client_id, @@ -308,7 +318,10 @@ def __init__( def __call__(self, caching=True): if not self._data: - self._data = APIObject(self.get(self._endpoint, caching=caching), self) + self._data = APIObject( + self.get(self._endpoint, caching=caching), + self + ) return self._data def whoami(self): @@ -327,6 +340,10 @@ def refresh(self): self.expires = int(time.time()) + res['expires_in'] self._session.headers.update( {"Authorization": "Bearer %s" % self.token}) + + # trigger the signal + after_token_refresh.send(**res) + return self # for backwards compatibility def get(self, resource, params={}, caching=True): @@ -383,22 +400,33 @@ def __call__(self, **kwargs): # Caching is now handled by APIConnection if 'href' in self._dict: - method = kwargs.pop('method', 'get') # default to get: historic behaviour + # default to get: historic behaviour + method = kwargs.pop('method', 'get') data = kwargs.pop('data', {}) - caching = kwargs.pop('caching', True) # default caching to true, for get requests + # default caching to true, for get requests + caching = kwargs.pop('caching', True) # retain compatibility with historic method of passing parameters. - # Slightly unsatisfactory - what if data is dict-like but not a dict? + # Slightly unsatisfactory; what if data is dict-like but not a dict if isinstance(data, dict): for arg in kwargs: data[arg] = kwargs[arg] if method == 'post': - return APIObject(self.connection.post(self._dict['href'], data=data), self.connection) + return APIObject( + self.connection.post(self._dict['href'], data=data), + self.connection + ) elif method == 'put': - return APIObject(self.connection.put(self._dict['href'], data=data), self.connection) + return APIObject( + self.connection.put(self._dict['href'], data=data), + self.connection + ) elif method == 'delete': - return APIObject(self.connection.delete(self._dict['href']), self.connection) + return APIObject( + self.connection.delete(self._dict['href']), + self.connection + ) elif method == 'get': return APIObject(self.connection.get(self._dict['href'], params=data, diff --git a/pycrest/events.py b/pycrest/events.py new file mode 100644 index 0000000..82f15f7 --- /dev/null +++ b/pycrest/events.py @@ -0,0 +1,58 @@ +# -*- encoding: utf-8 -*- +import logging +import sys + +logger = logging.getLogger("pycrest.events") + + +class Signal(object): + def __init__(self): + """ Alarm constructor. """ + self.event_receivers = [] + + def add_receiver(self, receiver): + """ Add a receiver to the list of receivers. + + :param receiver: a callable variable + """ + if not callable(receiver): + raise TypeError("receiver must be callable") + self.event_receivers.append(receiver) + + def remove_receiver(self, receiver): + """ Remove a receiver to the list of receivers. + + :param receiver: a callable variable + """ + if receiver in self.event_receivers: + self.event_receivers.remove(receiver) + + def send(self, **kwargs): + """ Trigger all receiver and pass them the parameters + If an exception is raised, it will stop the process and all receivers + may not be triggered at this moment. + + :param kwargs: all arguments from the event. + """ + for receiver in self.event_receivers: + receiver(**kwargs) + + def send_robust(self, **kwargs): + """ Trigger all receiver and pass them the parameters + If an exception is raised it will be catched and displayed as error + in the logger (if defined). + + :param kwargs: all arguments from the event. + """ + for receiver in self.event_receivers: + try: + receiver(**kwargs) + except Exception as err: + if not hasattr(err, '__traceback__'): + logger.error(sys.exc_info()[2]) + else: + logger.error(err.__traceback__) + + +# define required alarms +after_token_refresh = Signal() diff --git a/tests/test_events.py b/tests/test_events.py new file mode 100644 index 0000000..fbe31dd --- /dev/null +++ b/tests/test_events.py @@ -0,0 +1,45 @@ +from pycrest.events import Signal + +import unittest + + +class TestSignal(unittest.TestCase): + EVENT_ARG = "testing" + + def event_receiver(self, example_arg): + self.assertEqual(example_arg, TestSignal.EVENT_ARG) + + def event_receiver_exception(self): + raise AssertionError("Triggered") + + def setUp(self): + self.signal = Signal() + + def test_signal_send(self): + self.signal.add_receiver(self.event_receiver) + self.signal.send(example_arg=TestSignal.EVENT_ARG) + + def test_signal_send_exception(self): + self.signal.add_receiver(self.event_receiver_exception) + with self.assertRaises(AssertionError): + self.signal.send() + + def test_signal_send_robust(self): + self.signal.add_receiver(self.event_receiver) + self.signal.send_robust(example_arg=TestSignal.EVENT_ARG) + + def test_signal_send_robust_exception(self): + self.signal.add_receiver(self.event_receiver_exception) + self.signal.send_robust() + + def test_signal_add_remove_receiver(self): + self.signal.add_receiver(self.event_receiver_exception) + self.assertIn( + self.event_receiver_exception, + self.signal.event_receivers + ) + self.signal.remove_receiver(self.event_receiver_exception) + self.assertNotIn( + self.event_receiver_exception, + self.signal.event_receivers + ) diff --git a/tests/test_pycrest.py b/tests/test_pycrest.py index 0ea6c71..69c0d7e 100644 --- a/tests/test_pycrest.py +++ b/tests/test_pycrest.py @@ -3,22 +3,19 @@ @author: henk ''' -import errno import httmock import mock import pycrest -import sys import unittest -from pycrest.cache import APICache from pycrest.cache import DictCache from pycrest.cache import DummyCache from pycrest.cache import FileCache -from pycrest.cache import MemcachedCache from pycrest.errors import APIException from pycrest.errors import UnsupportedHTTPMethodException from pycrest.eve import APIObject from pycrest.eve import EVE +from pycrest.events import after_token_refresh from requests.adapters import HTTPAdapter from requests.models import PreparedRequest @@ -215,6 +212,13 @@ def test_whoami(self): self.authed.whoami() def test_refresh(self): + def refresh_event_receiver(access_token, refresh_token, expires_in): + """ fake event receiver to test the signal trigger """ + self.assertEqual(access_token, 'access_token') + self.assertEqual(refresh_token, 'refresh_token') + self.assertEqual(expires_in, 300) + after_token_refresh.add_receiver(refresh_event_receiver) + with httmock.HTTMock(*all_httmocks): self.authed.refresh() @@ -225,6 +229,7 @@ def test_refresh_on_get(self): class TestAPIConnection(unittest.TestCase): + TEST_DIR = '/tmp/TestFileCache' def setUp(self): self.api = EVE() @@ -337,9 +342,9 @@ def test_apicache(self): @mock.patch('os.path.isdir', return_value=False) @mock.patch('os.mkdir') def test_file_cache(self, mkdir_function, isdir_function): - file_cache = FileCache(path=TestFileCache.DIR) + file_cache = FileCache(path=TestAPIConnection.TEST_DIR) eve = EVE(cache=file_cache) - self.assertEqual(file_cache.path, TestFileCache.DIR) + self.assertEqual(file_cache.path, TestAPIConnection.TEST_DIR) self.assertTrue(isinstance(eve.cache, FileCache)) def test_default_url(self): From 1cbd2366f98d777a1be43a8789cc103231ac012f Mon Sep 17 00:00:00 2001 From: Guillaume Date: Wed, 30 Nov 2016 15:42:19 +0100 Subject: [PATCH 5/5] Add missing test in test_events --- tests/test_events.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_events.py b/tests/test_events.py index fbe31dd..63a443e 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -43,3 +43,7 @@ def test_signal_add_remove_receiver(self): self.event_receiver_exception, self.signal.event_receivers ) + + def test_signal_add_not_callable_receiver(self): + with self.assertRaises(TypeError): + self.signal.add_receiver("No callable receiver")