From 943971bbfed391c9dee6e52c06409732d0892f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=BBmuda?= Date: Mon, 6 May 2024 17:12:18 +0200 Subject: [PATCH] [#20] Removed support for Tornado --- CHANGELOG.rst | 5 + README.rst | 72 +---- docs/source/memoize.rst | 16 - examples/basic/{basic_asyncio.py => basic.py} | 6 +- examples/basic/basic_tornado.py | 26 -- .../{dogpiling_asyncio.py => dogpiling.py} | 6 +- examples/dogpiling/dogpiling_tornado.py | 45 --- examples/invalidation/invalidation.py | 6 +- examples/ttl/{ttl_asyncio.py => ttl.py} | 8 +- memoize/coerced.py | 152 --------- memoize/memoize_configuration.py | 8 - memoize/statuses.py | 9 +- memoize/wrapper.py | 24 +- mypy.ini | 2 + setup.py | 3 +- tests/__init__.py | 19 +- tests/{asynciotests => end2end}/__init__.py | 0 .../test_showcase.py | 36 +-- .../test_wrapper.py} | 141 ++++----- .../test_wrapper_manually_applied.py} | 126 +++----- tests/tornadotests/__init__.py | 0 tests/tornadotests/test_wrapper.py | 291 ------------------ tests/unit/test_eviction.py | 137 ++++----- tests/unit/test_invalidation.py | 56 ++-- tests/unit/test_key.py | 86 +++--- tests/unit/test_postprocessing.py | 32 +- tests/unit/test_serde.py | 81 +++-- tests/unit/test_statuses.py | 69 ++--- tests/unit/test_storage.py | 40 ++- tox.ini | 53 ++-- 30 files changed, 409 insertions(+), 1146 deletions(-) rename examples/basic/{basic_asyncio.py => basic.py} (72%) delete mode 100644 examples/basic/basic_tornado.py rename examples/dogpiling/{dogpiling_asyncio.py => dogpiling.py} (93%) delete mode 100644 examples/dogpiling/dogpiling_tornado.py rename examples/ttl/{ttl_asyncio.py => ttl.py} (93%) delete mode 100644 memoize/coerced.py delete mode 100644 memoize/memoize_configuration.py create mode 100644 mypy.ini rename tests/{asynciotests => end2end}/__init__.py (100%) rename tests/{asynciotests => end2end}/test_showcase.py (72%) rename tests/{asynciotests/test_wrapper_on_asyncio.py => end2end/test_wrapper.py} (67%) rename tests/{asynciotests/test_wrapper_manually_applied_on_asyncio.py => end2end/test_wrapper_manually_applied.py} (72%) delete mode 100644 tests/tornadotests/__init__.py delete mode 100644 tests/tornadotests/test_wrapper.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 94a4c90..d5b6529 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,8 @@ +3.0.0 +----- + +* Removed support for Tornado + 2.1.0 ----- diff --git a/README.rst b/README.rst index 9490e9b..b7d1a4b 100644 --- a/README.rst +++ b/README.rst @@ -18,7 +18,7 @@ Extended docs (including API docs) available at `memoize.readthedocs.io `_ ) @@ -55,12 +55,6 @@ To get you up & running all you need is to install: Installation of Extras ~~~~~~~~~~~~~~~~~~~~~~ -If you are going to use ``memoize`` with tornado add a dependency on extra: - -.. code-block:: bash - - pip install py-memoize[tornado] - To harness the power of `ujson `_ (if JSON SerDe is used) install extra: .. code-block:: bash @@ -73,19 +67,13 @@ Usage Provided examples use default configuration to cache results in memory. For configuration options see `Configurability`_. -You can use ``memoize`` with both `asyncio `_ -and `Tornado `_ - please see the appropriate example: - -.. warning:: - Support for `Tornado `_ is planned to be removed in the future. - asyncio ~~~~~~~ To apply default caching configuration use: .. - _example_source: examples/basic/basic_asyncio.py + _example_source: examples/basic/basic.py .. code-block:: python @@ -109,45 +97,6 @@ To apply default caching configuration use: asyncio.get_event_loop().run_until_complete(main()) -Tornado -~~~~~~~ - -If your project is based on Tornado use: - -.. - _example_source: examples/basic/basic_tornado.py - -.. code-block:: python - - import random - - from tornado import gen - from tornado.ioloop import IOLoop - - from memoize.wrapper import memoize - - - @memoize() - @gen.coroutine - def expensive_computation(): - return 'expensive-computation-' + str(random.randint(1, 100)) - - - @gen.coroutine - def main(): - result1 = yield expensive_computation() - print(result1) - result2 = yield expensive_computation() - print(result2) - result3 = yield expensive_computation() - print(result3) - - - if __name__ == "__main__": - IOLoop.current().run_sync(main) - - - Features ======== @@ -162,21 +111,6 @@ This library is built async-oriented from the ground-up, what manifests in, for in `Dog-piling proofness`_ or `Async cache storage`_. -Tornado & asyncio support -------------------------- - -No matter what are you using, build-in `asyncio `_ -or its predecessor `Tornado `_ -*memoize* has you covered as you can use it with both. -**This may come handy if you are planning a migration from Tornado to asyncio.** - -Under the hood *memoize* detects if you are using *Tornado* or *asyncio* -(by checking if *Tornado* is installed and available to import). - -If have *Tornado* installed but your application uses *asyncio* IO-loop, -set ``MEMOIZE_FORCE_ASYNCIO=1`` environment variable to force using *asyncio* and ignore *Tornado* instalation. - - Configurability --------------- @@ -310,7 +244,7 @@ On failure, all requesters get an exception (same happens on timeout). An example of what it all is about: .. - _example_source: examples/dogpiling/dogpiling_asyncio.py + _example_source: examples/dogpiling/dogpiling.py .. code-block:: python diff --git a/docs/source/memoize.rst b/docs/source/memoize.rst index 879f2ec..5bbdded 100644 --- a/docs/source/memoize.rst +++ b/docs/source/memoize.rst @@ -4,14 +4,6 @@ memoize package Submodules ---------- -memoize.coerced module ----------------------- - -.. automodule:: memoize.coerced - :members: - :undoc-members: - :show-inheritance: - memoize.configuration module ---------------------------- @@ -68,14 +60,6 @@ memoize.key module :undoc-members: :show-inheritance: -memoize.memoize\_configuration module -------------------------------------- - -.. automodule:: memoize.memoize_configuration - :members: - :undoc-members: - :show-inheritance: - memoize.postprocessing module ----------------------------- diff --git a/examples/basic/basic_asyncio.py b/examples/basic/basic.py similarity index 72% rename from examples/basic/basic_asyncio.py rename to examples/basic/basic.py index bbe9d75..6234bde 100644 --- a/examples/basic/basic_asyncio.py +++ b/examples/basic/basic.py @@ -1,10 +1,6 @@ -from memoize import memoize_configuration - -# needed if one has tornado installed (could be removed otherwise) -memoize_configuration.force_asyncio = True - import asyncio import random + from memoize.wrapper import memoize diff --git a/examples/basic/basic_tornado.py b/examples/basic/basic_tornado.py deleted file mode 100644 index dd1909b..0000000 --- a/examples/basic/basic_tornado.py +++ /dev/null @@ -1,26 +0,0 @@ -import random - -from tornado import gen -from tornado.ioloop import IOLoop - -from memoize.wrapper import memoize - - -@memoize() -@gen.coroutine -def expensive_computation(): - return 'expensive-computation-' + str(random.randint(1, 100)) - - -@gen.coroutine -def main(): - result1 = yield expensive_computation() - print(result1) - result2 = yield expensive_computation() - print(result2) - result3 = yield expensive_computation() - print(result3) - - -if __name__ == "__main__": - IOLoop.current().run_sync(main) diff --git a/examples/dogpiling/dogpiling_asyncio.py b/examples/dogpiling/dogpiling.py similarity index 93% rename from examples/dogpiling/dogpiling_asyncio.py rename to examples/dogpiling/dogpiling.py index 68ef937..5ddf879 100644 --- a/examples/dogpiling/dogpiling_asyncio.py +++ b/examples/dogpiling/dogpiling.py @@ -1,8 +1,3 @@ -from memoize import memoize_configuration - -# needed if one has tornado installed (could be removed otherwise) -memoize_configuration.force_asyncio = True - import asyncio from datetime import timedelta @@ -54,5 +49,6 @@ async def main(): # Other cache generated 85 unique backend calls # Predicted (according to TTL) 17 unique backend calls + if __name__ == "__main__": asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/dogpiling/dogpiling_tornado.py b/examples/dogpiling/dogpiling_tornado.py deleted file mode 100644 index bd6c739..0000000 --- a/examples/dogpiling/dogpiling_tornado.py +++ /dev/null @@ -1,45 +0,0 @@ -from datetime import timedelta - -from tornado import gen -from tornado.ioloop import IOLoop - -from memoize.configuration import DefaultInMemoryCacheConfiguration -from memoize.wrapper import memoize - -# scenario configuration -concurrent_requests = 5 -request_batches_execution_count = 50 -cached_value_ttl_ms = 200 -delay_between_request_batches_ms = 70 - -# results/statistics -unique_calls_under_memoize = 0 - - -@memoize(configuration=(DefaultInMemoryCacheConfiguration(update_after=timedelta(milliseconds=cached_value_ttl_ms)))) -@gen.coroutine -def cached_with_memoize(): - global unique_calls_under_memoize - unique_calls_under_memoize += 1 - yield gen.sleep(0.01) - return unique_calls_under_memoize - - -@gen.coroutine -def main(): - for i in range(request_batches_execution_count): - res = yield [x() for x in [cached_with_memoize] * concurrent_requests] - print(res) - # yield [x() for x in [cached_with_different_cache] * concurrent_requests] - yield gen.sleep(delay_between_request_batches_ms / 1000) - - print("Memoize generated {} unique backend calls".format(unique_calls_under_memoize)) - predicted = (delay_between_request_batches_ms * request_batches_execution_count) // cached_value_ttl_ms - print("Predicted (according to TTL) {} unique backend calls".format(predicted)) - - # Printed: - # Memoize generated 17 unique backend calls - # Predicted (according to TTL) 17 unique backend calls - -if __name__ == "__main__": - IOLoop.current().run_sync(main) diff --git a/examples/invalidation/invalidation.py b/examples/invalidation/invalidation.py index 932ef3e..f47d675 100644 --- a/examples/invalidation/invalidation.py +++ b/examples/invalidation/invalidation.py @@ -1,11 +1,6 @@ -# needed if one has tornado installed (could be removed otherwise) -from memoize import memoize_configuration -memoize_configuration.force_asyncio = True - from memoize.configuration import DefaultInMemoryCacheConfiguration from memoize.invalidation import InvalidationSupport - import asyncio import random from memoize.wrapper import memoize @@ -43,5 +38,6 @@ async def main(): # Invalidation # 2 # expensive - computation - 59 + if __name__ == "__main__": asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/ttl/ttl_asyncio.py b/examples/ttl/ttl.py similarity index 93% rename from examples/ttl/ttl_asyncio.py rename to examples/ttl/ttl.py index d6223bd..8c36c34 100644 --- a/examples/ttl/ttl_asyncio.py +++ b/examples/ttl/ttl.py @@ -1,17 +1,13 @@ -# needed if one has tornado installed (could be removed otherwise) -from memoize import memoize_configuration -memoize_configuration.force_asyncio = True - -import datetime import asyncio +import datetime import random from dataclasses import dataclass -from memoize.wrapper import memoize from memoize.configuration import DefaultInMemoryCacheConfiguration, MutableCacheConfiguration from memoize.entry import CacheKey, CacheEntry from memoize.entrybuilder import CacheEntryBuilder from memoize.storage import LocalInMemoryCacheStorage +from memoize.wrapper import memoize @dataclass diff --git a/memoize/coerced.py b/memoize/coerced.py deleted file mode 100644 index b779597..0000000 --- a/memoize/coerced.py +++ /dev/null @@ -1,152 +0,0 @@ -""" -[Internal use only] Some functions work differently way depending if asyncio or Tornado is used - this is resolved here. -""" - -import datetime -import importlib.util -import logging -import sys - -from memoize.memoize_configuration import force_asyncio - -logger = logging.getLogger('memoize') -try: - if force_asyncio and importlib.util.find_spec('tornado'): - logger.warning('Forcefully switching to asyncio even though tornado exists') - raise ImportError() - - from tornado.ioloop import IOLoop - from tornado import gen - # ignore for mypy as we don't know any stub source for tornado.platform.asyncio - from tornado.platform.asyncio import to_asyncio_future # type: ignore - from tornado.concurrent import Future - - logger.info('Passed tornado availability check - using tornado') - - # ignore for mypy as types are resolved in runtime - def _apply_timeout(method_timeout: datetime.timedelta, future: Future) -> Future: # type: ignore - return gen.with_timeout(method_timeout, future) - - - def _call_later(delay: datetime.timedelta, callback): - IOLoop.current().call_later(delay=delay.total_seconds(), callback=callback) - - - def _call_soon(callback, *args): - return IOLoop.current().spawn_callback(callback, *args) - - - def _future(): - return Future() - - - def _timeout_error_type(): - return gen.TimeoutError - -except ImportError: - import asyncio - - logger.info('Using asyncio instead of torando') - - # this backported version of `wait_for` is taken from Python 3.11 - # and allows to continue having these `coerced` functions working (they are at least partially based on hacks) - # in general we need to drop tornado support either way (so this temporary solution would be gone either way) - async def wait_for(fut, timeout): - """Wait for the single Future or coroutine to complete, with timeout. - - Coroutine will be wrapped in Task. - - Returns result of the Future or coroutine. When a timeout occurs, - it cancels the task and raises TimeoutError. To avoid the task - cancellation, wrap it in shield(). - - If the wait is cancelled, the task is also cancelled. - - This function is a coroutine. - """ - - from asyncio import events, ensure_future, exceptions - from asyncio.tasks import _cancel_and_wait, _release_waiter - import functools - loop = events.get_running_loop() - - if timeout is None: - return await fut - - if timeout <= 0: - fut = ensure_future(fut, loop=loop) - - if fut.done(): - return fut.result() - - await _cancel_and_wait(fut) - try: - return fut.result() - except exceptions.CancelledError as exc: - raise exceptions.TimeoutError() from exc - - waiter = loop.create_future() - timeout_handle = loop.call_later(timeout, _release_waiter, waiter) - cb = functools.partial(_release_waiter, waiter) - - fut = ensure_future(fut, loop=loop) - fut.add_done_callback(cb) - - try: - # wait until the future completes or the timeout - try: - await waiter - except exceptions.CancelledError: - if fut.done(): - return fut.result() - else: - fut.remove_done_callback(cb) - # We must ensure that the task is not running - # after wait_for() returns. - # See https://bugs.python.org/issue32751 - await _cancel_and_wait(fut) - raise - - if fut.done(): - return fut.result() - else: - fut.remove_done_callback(cb) - # We must ensure that the task is not running - # after wait_for() returns. - # See https://bugs.python.org/issue32751 - await _cancel_and_wait(fut) - # In case task cancellation failed with some - # exception, we should re-raise it - # See https://bugs.python.org/issue40607 - try: - return fut.result() - except exceptions.CancelledError as exc: - raise exceptions.TimeoutError() from exc - finally: - timeout_handle.cancel() - - # ignore for mypy as types are resolved in runtime - def _apply_timeout(method_timeout: datetime.timedelta, future: asyncio.Future) -> asyncio.Future: # type: ignore - if sys.version_info >= (3, 12, 0): - return wait_for(future, method_timeout.total_seconds()) - else: - return asyncio.wait_for(future, method_timeout.total_seconds()) - - - def _call_later(delay: datetime.timedelta, callback): - asyncio.get_event_loop().call_later(delay=delay.total_seconds(), callback=callback) - - - def _call_soon(callback, *args): - asyncio.get_event_loop().call_soon(asyncio.ensure_future, callback(*args)) - - - def _future(): - return asyncio.Future() - - - def _timeout_error_type(): - try: - return asyncio.futures.TimeoutError - except AttributeError: - return asyncio.TimeoutError diff --git a/memoize/memoize_configuration.py b/memoize/memoize_configuration.py deleted file mode 100644 index a3cccdf..0000000 --- a/memoize/memoize_configuration.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -[API] Provides global config of the library. -Translates environment variables into values used internally by the library. -""" - -import os - -force_asyncio = bool(os.environ.get('MEMOIZE_FORCE_ASYNCIO', False)) diff --git a/memoize/statuses.py b/memoize/statuses.py index a6f3913..c91f64d 100644 --- a/memoize/statuses.py +++ b/memoize/statuses.py @@ -1,12 +1,12 @@ """ [Internal use only] Encapsulates update state management. """ +import asyncio import datetime import logging from asyncio import Future -from typing import Optional, Dict, Awaitable, Union +from typing import Dict, Awaitable, Union -from memoize import coerced from memoize.entry import CacheKey, CacheEntry @@ -27,7 +27,7 @@ def mark_being_updated(self, key: CacheKey) -> None: if key in self._updates_in_progress: raise ValueError('Key {} is already being updated'.format(key)) - future = coerced._future() + future: Future = asyncio.Future() self._updates_in_progress[key] = future def complete_on_timeout_passed(): @@ -38,7 +38,8 @@ def complete_on_timeout_passed(): self._updates_in_progress[key].set_result(None) self._updates_in_progress.pop(key) - coerced._call_later(self._update_lock_timeout, complete_on_timeout_passed) + asyncio.get_event_loop().call_later(delay=self._update_lock_timeout.total_seconds(), + callback=complete_on_timeout_passed) def mark_updated(self, key: CacheKey, entry: CacheEntry) -> None: """Informs that update has been finished. diff --git a/memoize/wrapper.py b/memoize/wrapper.py index 3e20226..4379e6b 100644 --- a/memoize/wrapper.py +++ b/memoize/wrapper.py @@ -6,9 +6,9 @@ import datetime import functools import logging +from asyncio import Future from typing import Optional, Callable -from memoize.coerced import _apply_timeout, _call_soon, _timeout_error_type from memoize.configuration import CacheConfiguration, NotConfiguredCacheCalledException, \ DefaultInMemoryCacheConfiguration, MutableCacheConfiguration from memoize.entry import CacheKey, CacheEntry @@ -98,10 +98,13 @@ async def refresh(actual_entry: Optional[CacheEntry], key: CacheKey, eviction_strategy.mark_written(key, offered_entry) to_release = eviction_strategy.next_to_release() if to_release is not None: - _call_soon(try_release, to_release, configuration_snapshot) + asyncio.get_event_loop().call_soon( + asyncio.ensure_future, + try_release(to_release, configuration_snapshot) + ) return offered_entry - except (asyncio.TimeoutError, _timeout_error_type()) as e: + except asyncio.TimeoutError as e: logger.debug('Timeout for %s: %s', key, e) update_statuses.mark_update_aborted(key, e) raise CachedMethodFailedException('Refresh timed out') from e @@ -120,14 +123,18 @@ async def wrapper(*args, **kwargs): force_refresh = kwargs.pop('force_refresh_memoized', False) key = configuration_snapshot.key_extractor().format_key(method, args, kwargs) - current_entry = await configuration_snapshot.storage().get(key) # type: Optional[CacheEntry] + current_entry: Optional[CacheEntry] = await configuration_snapshot.storage().get(key) if current_entry is not None: configuration_snapshot.eviction_strategy().mark_read(key) now = datetime.datetime.now(datetime.timezone.utc) - def value_future_provider(): - return _apply_timeout(configuration_snapshot.method_timeout(), method(*args, **kwargs)) + def value_future_provider() -> Future: + # applying timeout to the method call + return asyncio.ensure_future(asyncio.wait_for( + method(*args, **kwargs), + configuration_snapshot.method_timeout().total_seconds() + )) if current_entry is None: logger.debug('Creating (blocking) entry for key %s', key) @@ -140,7 +147,10 @@ def value_future_provider(): result = await refresh(None, key, value_future_provider, configuration_snapshot) elif current_entry.update_after <= now: logger.debug('Entry update point expired - entry update (async - current entry returned) for key %s', key) - _call_soon(refresh, current_entry, key, value_future_provider, configuration_snapshot) + asyncio.get_event_loop().call_soon( + asyncio.ensure_future, + refresh(current_entry, key, value_future_provider, configuration_snapshot) + ) result = current_entry else: result = current_entry diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..db14311 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +no_implicit_optional=False \ No newline at end of file diff --git a/setup.py b/setup.py index e6ff7bc..a565190 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def prepare_description(): setup( name='py-memoize', - version='2.1.0', + version='3.0.0', author='Michal Zmuda', author_email='zmu.michal@gmail.com', url='https://github.com/DreamLab/memoize', @@ -27,7 +27,6 @@ def prepare_description(): keywords='python cache tornado asyncio', install_requires=None, extras_require={ - 'tornado': ['tornado>4,<5'], 'ujson': ['ujson>=1.35,<2'], }, classifiers=[ diff --git a/tests/__init__.py b/tests/__init__.py index 2711d03..05075a9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,10 +1,8 @@ import asyncio -from tornado import gen -from tornado.concurrent import Future def _as_future(value): - future = Future() + future = asyncio.Future() if isinstance(value, Exception): future.set_exception(value) else: @@ -12,11 +10,10 @@ def _as_future(value): return future -@gen.coroutine -def _ensure_background_tasks_finished(): +async def _ensure_background_tasks_finished(): # let other tasks to acquire IO loop for i in range(10): - yield gen.sleep(0) + await asyncio.sleep(0) async def _ensure_asyncio_background_tasks_finished(): @@ -30,20 +27,18 @@ def __eq__(self, o: object) -> bool: return True -def _assert_called_once_with(test_object, mock, args, kwargs): +def _assert_called_once_with(mock, args, kwargs): """Mock assertions do not support "any" placeholders. This method allows to use AnyObject as any argument.""" calls_list = mock.call_args_list - test_object.assertEqual(1, len(calls_list), 'Called {} times but expected only one call'.format(len(calls_list))) + assert 1 == len(calls_list), 'Called {} times but expected only one call'.format(len(calls_list)) call_args, call_kwargs = calls_list[0] for arg_num in range(len(args)): expected = args[arg_num] got = call_args[arg_num] - test_object.assertTrue(expected.__eq__(got), 'Arguments at position {} mismatch: {} != {}' - .format(arg_num, expected, got)) + assert expected.__eq__(got), 'Arguments at position {} mismatch: {} != {}'.format(arg_num, expected, got) for kwarg_key in set(kwargs.keys()).union(set(call_kwargs.keys())): expected = args[kwarg_key] got = call_args[kwarg_key] - test_object.assertTrue(expected.__eq__(got), 'Arguments under key {} mismatch: {} != {}' - .format(kwarg_key, expected, got)) + assert expected.__eq__(got), 'Arguments under key {} mismatch: {} != {}'.format(kwarg_key, expected, got) diff --git a/tests/asynciotests/__init__.py b/tests/end2end/__init__.py similarity index 100% rename from tests/asynciotests/__init__.py rename to tests/end2end/__init__.py diff --git a/tests/asynciotests/test_showcase.py b/tests/end2end/test_showcase.py similarity index 72% rename from tests/asynciotests/test_showcase.py rename to tests/end2end/test_showcase.py index 7402623..79fef73 100644 --- a/tests/asynciotests/test_showcase.py +++ b/tests/end2end/test_showcase.py @@ -1,12 +1,7 @@ -from tests.py310workaround import fix_python_3_10_compatibility - -fix_python_3_10_compatibility() - import time from datetime import timedelta -import tornado -from tornado.testing import AsyncTestCase, gen_test +import pytest from memoize.configuration import DefaultInMemoryCacheConfiguration from memoize.exceptions import CachedMethodFailedException @@ -14,14 +9,8 @@ from tests import _ensure_asyncio_background_tasks_finished -class MemoizationTests(AsyncTestCase): - - def get_new_ioloop(self): - return tornado.platform.asyncio.AsyncIOMainLoop() - - def setUp(self): - self.maxDiff = None - super().setUp() +@pytest.mark.asyncio(scope="class") +class TestShowcase: # overriding this as background refreshes that failed # with default _handle_exception implementation cause test-case failure despite assertions passing @@ -29,7 +18,6 @@ def _handle_exception(self, typ, value, tb): import logging logging.warning("Loose exception - see it is related to background refreshes that failed %s", value) - @gen_test async def test_complex_showcase(self): # given UPDATE_MS = 400.0 @@ -84,15 +72,15 @@ async def get_value_or_throw(arg, kwarg=None): time.sleep(EXPIRE_S) await _ensure_asyncio_background_tasks_finished() value, should_throw = 'throws #4', True - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as context: await get_value_or_throw('test', kwarg='args') # then - self.assertEqual('ok #1', res1) - self.assertEqual('ok #1', res2) # previous value - refresh in background - self.assertEqual('ok #2', res3) # value from cache - still relevant - self.assertEqual('ok #2', res4) # value from cache - still relevant - self.assertEqual('ok #2', res5) # stale from cache - refresh in background - self.assertEqual('ok #2', res6) # stale from cache - should be updated but method throws - self.assertEqual(str(context.exception), str(CachedMethodFailedException('Refresh failed to complete'))) - self.assertEqual(str(context.exception.__cause__), str(ValueError("throws #4"))) + assert 'ok #1' == res1 + assert 'ok #1' == res2 # previous value - refresh in background + assert 'ok #2' == res3 # value from cache - still relevant + assert 'ok #2' == res4 # value from cache - still relevant + assert 'ok #2' == res5 # stale from cache - refresh in background + assert 'ok #2' == res6 # stale from cache - should be updated but method throws + assert str(context.value) == str(CachedMethodFailedException('Refresh failed to complete')) + assert str(context.value.__cause__) == str(ValueError("throws #4")) diff --git a/tests/asynciotests/test_wrapper_on_asyncio.py b/tests/end2end/test_wrapper.py similarity index 67% rename from tests/asynciotests/test_wrapper_on_asyncio.py rename to tests/end2end/test_wrapper.py index 2fbaa13..9454545 100644 --- a/tests/asynciotests/test_wrapper_on_asyncio.py +++ b/tests/end2end/test_wrapper.py @@ -1,18 +1,10 @@ -from memoize.coerced import _timeout_error_type -from tests.py310workaround import fix_python_3_10_compatibility - -fix_python_3_10_compatibility() - import asyncio import time from datetime import timedelta from unittest.mock import Mock -import tornado -from tornado.platform.asyncio import to_asyncio_future -from tornado.testing import AsyncTestCase, gen_test +import pytest -from memoize import memoize_configuration from memoize.configuration import MutableCacheConfiguration, NotConfiguredCacheCalledException, \ DefaultInMemoryCacheConfiguration from memoize.eviction import LeastRecentlyUpdatedEvictionStrategy @@ -22,16 +14,13 @@ from tests import _ensure_asyncio_background_tasks_finished -class MemoizationTests(AsyncTestCase): - - def get_new_ioloop(self): - return tornado.platform.asyncio.AsyncIOMainLoop() +@pytest.mark.asyncio(scope="class") +class TestWrapper: def setUp(self): self.maxDiff = None super().setUp() - @gen_test async def test_should_return_cached_value_on_expiration_time_not_reached(self): # given value = 0 @@ -50,10 +39,9 @@ async def get_value(arg, kwarg=None): res2 = await self._call_thrice(lambda: get_value('test', kwarg='args')) # then - self.assertEqual(0, res1) - self.assertEqual([0, 0, 0], res2) + assert 0 == res1 + assert [0, 0, 0] == res2 - @gen_test async def test_should_return_updated_value_on_expiration_time_reached(self): # given value = 0 @@ -72,10 +60,9 @@ async def get_value(arg, kwarg=None): res2 = await self._call_thrice(lambda: get_value('test', kwarg='args')) # then - self.assertEqual(0, res1) - self.assertEqual([1, 1, 1], res2) + assert 0 == res1 + assert [1, 1, 1] == res2 - @gen_test async def test_should_return_current_value_on_first_call_after_update_time_reached_but_not_expiration_time(self): # given value = 0 @@ -94,10 +81,9 @@ async def get_value(arg, kwarg=None): res2 = await self._call_thrice(lambda: get_value('test', kwarg='args')) # then - self.assertEqual(0, res1) - self.assertEqual([0, 0, 0], res2) + assert 0 == res1 + assert [0, 0, 0] == res2 - @gen_test async def test_should_return_current_value_on_second_call_after_update_time_reached_but_not_expiration_time(self): # given value = 0 @@ -118,10 +104,9 @@ async def get_value(arg, kwarg=None): res2 = await self._call_thrice(lambda: get_value('test', kwarg='args')) # then - self.assertEqual(0, res1) - self.assertEqual([1, 1, 1], res2) + assert 0 == res1 + assert [1, 1, 1] == res2 - @gen_test async def test_should_return_different_values_on_different_args_with_default_key(self): # given value = 0 @@ -138,10 +123,9 @@ async def get_value(arg, kwarg=None): res2 = await get_value('test2', kwarg='args') # then - self.assertEqual(0, res1) - self.assertEqual(1, res2) + assert 0 == res1 + assert 1 == res2 - @gen_test async def test_should_return_different_values_on_different_kwargs_with_default_key(self): # given value = 0 @@ -158,10 +142,9 @@ async def get_value(arg, kwarg=None): res2 = await get_value('test', kwarg='args2') # then - self.assertEqual(0, res1) - self.assertEqual(1, res2) + assert 0 == res1 + assert 1 == res2 - @gen_test async def test_should_return_exception_for_all_concurrent_callers(self): # given value = 0 @@ -176,22 +159,21 @@ async def get_value(arg, kwarg=None): res3 = get_value('test', kwarg='args1') # then - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as context: await res1 - self.assertEqual(context.exception.__class__, CachedMethodFailedException) - self.assertEqual(str(context.exception.__cause__), str(ValueError('stub0'))) + assert context.value.__class__ == CachedMethodFailedException + assert str(context.value.__cause__) == str(ValueError('stub0')) - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as context: await res2 - self.assertEqual(context.exception.__class__, CachedMethodFailedException) - self.assertEqual(str(context.exception.__cause__), str(ValueError('stub0'))) + assert context.value.__class__ == CachedMethodFailedException + assert str(context.value.__cause__) == str(ValueError('stub0')) - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as context: await res3 - self.assertEqual(context.exception.__class__, CachedMethodFailedException) - self.assertEqual(str(context.exception.__cause__), str(ValueError('stub0'))) + assert context.value.__class__ == CachedMethodFailedException + assert str(context.value.__cause__) == str(ValueError('stub0')) - @gen_test async def test_should_return_timeout_for_all_concurrent_callers(self): # given value = 0 @@ -209,22 +191,21 @@ async def get_value(arg, kwarg=None): res3 = get_value('test', kwarg='args1') # then - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as context: await res1 - self.assertEqual(context.exception.__class__, CachedMethodFailedException) - self.assertEqual(context.exception.__cause__.__class__, _timeout_error_type()) + assert context.value.__class__ == CachedMethodFailedException + assert context.value.__cause__.__class__ == asyncio.TimeoutError - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as context: await res2 - self.assertEqual(context.exception.__class__, CachedMethodFailedException) - self.assertEqual(context.exception.__cause__.__class__, _timeout_error_type()) + assert context.value.__class__ == CachedMethodFailedException + assert context.value.__cause__.__class__ == asyncio.TimeoutError - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as context: await res3 - self.assertEqual(context.exception.__class__, CachedMethodFailedException) - self.assertEqual(context.exception.__cause__.__class__, _timeout_error_type()) + assert context.value.__class__ == CachedMethodFailedException + assert context.value.__cause__.__class__ == asyncio.TimeoutError - @gen_test async def test_should_return_same_value_on_constant_key_function(self): # given value = 0 @@ -233,8 +214,8 @@ async def test_should_return_same_value_on_constant_key_function(self): @memoize( configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_key_extractor(key_extractor) + .initialized_with(DefaultInMemoryCacheConfiguration()) + .set_key_extractor(key_extractor) ) async def get_value(arg, kwarg=None): return value @@ -247,10 +228,9 @@ async def get_value(arg, kwarg=None): res2 = await get_value('test2', kwarg='args2') # then - self.assertEqual(0, res1) - self.assertEqual(0, res2) + assert 0 == res1 + assert 0 == res2 - @gen_test async def test_should_release_keys_on_caching_multiple_elements(self): # given value = 0 @@ -260,10 +240,10 @@ async def test_should_release_keys_on_caching_multiple_elements(self): @memoize( configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_eviction_strategy(LeastRecentlyUpdatedEvictionStrategy(capacity=2)) - .set_key_extractor(key_extractor) - .set_storage(storage) + .initialized_with(DefaultInMemoryCacheConfiguration()) + .set_eviction_strategy(LeastRecentlyUpdatedEvictionStrategy(capacity=2)) + .set_key_extractor(key_extractor) + .set_storage(storage) ) async def get_value(arg, kwarg=None): return value @@ -281,31 +261,29 @@ async def get_value(arg, kwarg=None): s3 = await storage.get("('test3', 'args3')") s4 = await storage.get("('test4', 'args4')") - self.assertIsNone(s1) - self.assertIsNone(s2) - self.assertIsNotNone(s3) - self.assertIsNotNone(s4) + assert s1 is None + assert s2 is None + assert s3 is not None + assert s4 is not None - @gen_test async def test_should_throw_exception_on_configuration_not_ready(self): # given @memoize( configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_configured(False) + .initialized_with(DefaultInMemoryCacheConfiguration()) + .set_configured(False) ) async def get_value(arg, kwarg=None): raise ValueError("Get lost") # when - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as context: await get_value('test1', kwarg='args1') # then expected = NotConfiguredCacheCalledException() - self.assertEqual(str(expected), str(context.exception)) + assert str(expected) == str(context.value) - @gen_test async def test_should_throw_exception_on_wrapped_method_failure(self): # given @memoize() @@ -313,14 +291,13 @@ async def get_value(arg, kwarg=None): raise ValueError("Get lost") # when - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as context: await get_value('test1', kwarg='args1') # then - self.assertEqual(str(context.exception), str(CachedMethodFailedException('Refresh failed to complete'))) - self.assertEqual(str(context.exception.__cause__), str(ValueError("Get lost"))) + assert str(context.value) == str(CachedMethodFailedException('Refresh failed to complete')) + assert str(context.value.__cause__) == str(ValueError("Get lost")) - @gen_test async def test_should_throw_exception_on_refresh_timeout(self): # given @@ -332,19 +309,13 @@ async def get_value(arg, kwarg=None): return 0 # when - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as context: await get_value('test1', kwarg='args1') # then - self.assertEqual(context.exception.__class__, CachedMethodFailedException) - self.assertEqual(context.exception.__cause__.__class__, _timeout_error_type()) + assert context.value.__class__ == CachedMethodFailedException + assert context.value.__cause__.__class__ == asyncio.TimeoutError @staticmethod async def _call_thrice(call): - # gen_test setup somehow interferes with coroutines and futures - # code tested manually works without such "decorations" but for testing this workaround was the bes I've found - if memoize_configuration.force_asyncio: - res2 = await asyncio.gather(call(), call(), call()) - else: - res2 = await asyncio.gather(to_asyncio_future(call()), to_asyncio_future(call()), to_asyncio_future(call())) - return res2 + return await asyncio.gather(call(), call(), call()) diff --git a/tests/asynciotests/test_wrapper_manually_applied_on_asyncio.py b/tests/end2end/test_wrapper_manually_applied.py similarity index 72% rename from tests/asynciotests/test_wrapper_manually_applied_on_asyncio.py rename to tests/end2end/test_wrapper_manually_applied.py index 1d71250..10f140f 100644 --- a/tests/asynciotests/test_wrapper_manually_applied_on_asyncio.py +++ b/tests/end2end/test_wrapper_manually_applied.py @@ -1,18 +1,10 @@ -from memoize.coerced import _timeout_error_type -from tests.py310workaround import fix_python_3_10_compatibility - -fix_python_3_10_compatibility() - import asyncio import time from datetime import timedelta from unittest.mock import Mock -import tornado -from tornado.platform.asyncio import to_asyncio_future -from tornado.testing import AsyncTestCase, gen_test +import pytest -from memoize import memoize_configuration from memoize.configuration import MutableCacheConfiguration, NotConfiguredCacheCalledException, \ DefaultInMemoryCacheConfiguration from memoize.eviction import LeastRecentlyUpdatedEvictionStrategy @@ -22,16 +14,9 @@ from tests import _ensure_asyncio_background_tasks_finished -class MemoizationTests(AsyncTestCase): - - def get_new_ioloop(self): - return tornado.platform.asyncio.AsyncIOMainLoop() +@pytest.mark.asyncio(scope="class") +class TestWrapperManuallyApplied: - def setUp(self): - self.maxDiff = None - super().setUp() - - @gen_test async def test_should_return_cached_value_on_expiration_time_not_reached(self): # given value = 0 @@ -46,17 +31,16 @@ async def get_value(arg, kwarg=None): # when res1 = await get_value_cached('test', kwarg='args') - time.sleep(.200) + await asyncio.sleep(0.200) await _ensure_asyncio_background_tasks_finished() value = 1 # calling thrice be more confident about behaviour of parallel execution res2 = await self._call_thrice(lambda: get_value_cached('test', kwarg='args')) # then - self.assertEqual(0, res1) - self.assertEqual([0, 0, 0], res2) + assert res1 == 0 + assert res2 == [0, 0, 0] - @gen_test async def test_should_return_updated_value_on_expiration_time_reached(self): # given value = 0 @@ -71,17 +55,16 @@ async def get_value(arg, kwarg=None): # when res1 = await get_value_cached('test', kwarg='args') - time.sleep(.200) + await asyncio.sleep(0.200) await _ensure_asyncio_background_tasks_finished() value = 1 # calling thrice be more confident about behaviour of parallel execution res2 = await self._call_thrice(lambda: get_value_cached('test', kwarg='args')) # then - self.assertEqual(0, res1) - self.assertEqual([1, 1, 1], res2) + assert res1 == 0 + assert res2 == [1, 1, 1] - @gen_test async def test_should_return_current_value_on_first_call_after_update_time_reached_but_not_expiration_time(self): # given value = 0 @@ -96,17 +79,16 @@ async def get_value(arg, kwarg=None): # when res1 = await get_value_cached('test', kwarg='args') - time.sleep(.200) + await asyncio.sleep(0.200) await _ensure_asyncio_background_tasks_finished() value = 1 # calling thrice be more confident about behaviour of parallel execution res2 = await self._call_thrice(lambda: get_value_cached('test', kwarg='args')) # then - self.assertEqual(0, res1) - self.assertEqual([0, 0, 0], res2) + assert res1 == 0 + assert res2 == [0, 0, 0] - @gen_test async def test_should_return_current_value_on_second_call_after_update_time_reached_but_not_expiration_time(self): # given value = 0 @@ -121,7 +103,7 @@ async def get_value(arg, kwarg=None): # when res1 = await get_value_cached('test', kwarg='args') - time.sleep(.200) + await asyncio.sleep(0.200) await _ensure_asyncio_background_tasks_finished() value = 1 await get_value_cached('test', kwarg='args') @@ -130,10 +112,9 @@ async def get_value(arg, kwarg=None): res2 = await self._call_thrice(lambda: get_value_cached('test', kwarg='args')) # then - self.assertEqual(0, res1) - self.assertEqual([1, 1, 1], res2) + assert res1 == 0 + assert res2 == [1, 1, 1] - @gen_test async def test_should_return_different_values_on_different_args_with_default_key(self): # given value = 0 @@ -145,16 +126,15 @@ async def get_value(arg, kwarg=None): # when res1 = await get_value_cached('test1', kwarg='args') - time.sleep(.200) + await asyncio.sleep(0.200) await _ensure_asyncio_background_tasks_finished() value = 1 res2 = await get_value_cached('test2', kwarg='args') # then - self.assertEqual(0, res1) - self.assertEqual(1, res2) + assert res1 == 0 + assert res2 == 1 - @gen_test async def test_should_return_different_values_on_different_kwargs_with_default_key(self): # given value = 0 @@ -166,16 +146,15 @@ async def get_value(arg, kwarg=None): # when res1 = await get_value_cached('test', kwarg='args1') - time.sleep(.200) + await asyncio.sleep(0.200) await _ensure_asyncio_background_tasks_finished() value = 1 res2 = await get_value_cached('test', kwarg='args2') # then - self.assertEqual(0, res1) - self.assertEqual(1, res2) + assert res1 == 0 + assert res2 == 1 - @gen_test async def test_should_return_same_value_on_constant_key_function(self): # given value = 0 @@ -188,22 +167,21 @@ async def get_value(arg, kwarg=None): get_value_cached = memoize( method=get_value, configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_key_extractor(key_extractor) + .initialized_with(DefaultInMemoryCacheConfiguration()) + .set_key_extractor(key_extractor) ) # when res1 = await get_value_cached('test1', kwarg='args1') - time.sleep(.200) + await asyncio.sleep(0.200) await _ensure_asyncio_background_tasks_finished() value = 1 res2 = await get_value_cached('test2', kwarg='args2') # then - self.assertEqual(0, res1) - self.assertEqual(0, res2) + assert res1 == 0 + assert res2 == 0 - @gen_test async def test_should_release_keys_on_caching_multiple_elements(self): # given value = 0 @@ -217,10 +195,10 @@ async def get_value(arg, kwarg=None): get_value_cached = memoize( method=get_value, configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_eviction_strategy(LeastRecentlyUpdatedEvictionStrategy(capacity=2)) - .set_key_extractor(key_extractor) - .set_storage(storage) + .initialized_with(DefaultInMemoryCacheConfiguration()) + .set_eviction_strategy(LeastRecentlyUpdatedEvictionStrategy(capacity=2)) + .set_key_extractor(key_extractor) + .set_storage(storage) ) # when @@ -236,12 +214,11 @@ async def get_value(arg, kwarg=None): s3 = await storage.get("('test3', 'args3')") s4 = await storage.get("('test4', 'args4')") - self.assertIsNone(s1) - self.assertIsNone(s2) - self.assertIsNotNone(s3) - self.assertIsNotNone(s4) + assert s1 is None + assert s2 is None + assert s3 is not None + assert s4 is not None - @gen_test async def test_should_throw_exception_on_configuration_not_ready(self): # given async def get_value(arg, kwarg=None): @@ -250,19 +227,18 @@ async def get_value(arg, kwarg=None): get_value_cached = memoize( method=get_value, configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_configured(False) + .initialized_with(DefaultInMemoryCacheConfiguration()) + .set_configured(False) ) # when - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as context: await get_value_cached('test1', kwarg='args1') # then expected = NotConfiguredCacheCalledException() - self.assertEqual(str(expected), str(context.exception)) + assert str(expected) == str(context.value) - @gen_test async def test_should_throw_exception_on_wrapped_method_failure(self): # given async def get_value(arg, kwarg=None): @@ -271,14 +247,13 @@ async def get_value(arg, kwarg=None): get_value_cached = memoize(method=get_value, configuration=DefaultInMemoryCacheConfiguration()) # when - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as context: await get_value_cached('test1', kwarg='args1') # then - self.assertEqual(str(context.exception), str(CachedMethodFailedException('Refresh failed to complete'))) - self.assertEqual(str(context.exception.__cause__), str(ValueError("Get lost"))) + assert str(context.value) == str(CachedMethodFailedException('Refresh failed to complete')) + assert str(context.value.__cause__) == str(ValueError("Get lost")) - @gen_test async def test_should_throw_exception_on_refresh_timeout(self): # given async def get_value(arg, kwarg=None): @@ -292,19 +267,12 @@ async def get_value(arg, kwarg=None): configuration=DefaultInMemoryCacheConfiguration(method_timeout=timedelta(milliseconds=100))) # when - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as context: await get_value_cached('test1', kwarg='args1') # then - self.assertEqual(context.exception.__class__, CachedMethodFailedException) - self.assertEqual(context.exception.__cause__.__class__, _timeout_error_type()) - - @staticmethod - async def _call_thrice(call): - # gen_test setup somehow interferes with coroutines and futures - # code tested manually works without such "decorations" but for testing this workaround was the bes I've found - if memoize_configuration.force_asyncio: - res2 = await asyncio.gather(call(), call(), call()) - else: - res2 = await asyncio.gather(to_asyncio_future(call()), to_asyncio_future(call()), to_asyncio_future(call())) - return res2 + assert context.value.__class__ == CachedMethodFailedException + assert context.value.__cause__.__class__ == asyncio.TimeoutError + + async def _call_thrice(self, call): + return await asyncio.gather(call(), call(), call()) diff --git a/tests/tornadotests/__init__.py b/tests/tornadotests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/tornadotests/test_wrapper.py b/tests/tornadotests/test_wrapper.py deleted file mode 100644 index 1d8f896..0000000 --- a/tests/tornadotests/test_wrapper.py +++ /dev/null @@ -1,291 +0,0 @@ -from memoize.coerced import _timeout_error_type -from tests.py310workaround import fix_python_3_10_compatibility - -fix_python_3_10_compatibility() - -import time -from datetime import timedelta -from unittest.mock import Mock - -from tornado import gen -from tornado.testing import AsyncTestCase, gen_test - -from memoize.configuration import MutableCacheConfiguration, NotConfiguredCacheCalledException, \ - DefaultInMemoryCacheConfiguration -from memoize.eviction import LeastRecentlyUpdatedEvictionStrategy -from memoize.exceptions import CachedMethodFailedException -from memoize.storage import LocalInMemoryCacheStorage -from memoize.wrapper import memoize -from tests import _ensure_background_tasks_finished - - -class MemoizationTests(AsyncTestCase): - - def setUp(self): - self.maxDiff = None - super().setUp() - - @gen_test - def test_should_return_cached_value_on_expiration_time_not_reached(self): - # given - value = 0 - - @memoize(configuration=DefaultInMemoryCacheConfiguration(update_after=timedelta(minutes=1), - expire_after=timedelta(minutes=2))) - @gen.coroutine - def get_value(arg, kwarg=None): - return value - - # when - res1 = yield get_value('test', kwarg='args') - time.sleep(.200) - yield _ensure_background_tasks_finished() - value = 1 - # calling thrice be more confident about behaviour of parallel execution - res2 = yield [get_value('test', kwarg='args'), - get_value('test', kwarg='args'), - get_value('test', kwarg='args'), ] - - # then - self.assertEqual(0, res1) - self.assertEqual([0, 0, 0], res2) - - @gen_test - def test_should_return_updated_value_on_expiration_time_reached(self): - # given - value = 0 - - @memoize(configuration=DefaultInMemoryCacheConfiguration(update_after=timedelta(milliseconds=50), - expire_after=timedelta(milliseconds=100))) - @gen.coroutine - def get_value(arg, kwarg=None): - return value - - # when - res1 = yield get_value('test', kwarg='args') - time.sleep(.200) - yield _ensure_background_tasks_finished() - value = 1 - # calling thrice be more confident about behaviour of parallel execution - res2 = yield [get_value('test', kwarg='args'), - get_value('test', kwarg='args'), - get_value('test', kwarg='args'), ] - - # then - self.assertEqual(0, res1) - self.assertEqual([1, 1, 1], res2) - - @gen_test - def test_should_return_current_value_on_first_call_after_update_time_reached_but_not_expiration_time(self): - # given - value = 0 - - @memoize(configuration=DefaultInMemoryCacheConfiguration(update_after=timedelta(milliseconds=100), - expire_after=timedelta(minutes=5))) - @gen.coroutine - def get_value(arg, kwarg=None): - return value - - # when - res1 = yield get_value('test', kwarg='args') - time.sleep(.200) - yield _ensure_background_tasks_finished() - value = 1 - # calling thrice be more confident about behaviour of parallel execution - res2 = yield [get_value('test', kwarg='args'), - get_value('test', kwarg='args'), - get_value('test', kwarg='args'), ] - - # then - self.assertEqual(0, res1) - self.assertEqual([0, 0, 0], res2) - - @gen_test - def test_should_return_current_value_on_second_call_after_update_time_reached_but_not_expiration_time(self): - # given - value = 0 - - @memoize(configuration=DefaultInMemoryCacheConfiguration(update_after=timedelta(milliseconds=100), - expire_after=timedelta(minutes=5))) - @gen.coroutine - def get_value(arg, kwarg=None): - return value - - # when - res1 = yield get_value('test', kwarg='args') - time.sleep(.200) - yield _ensure_background_tasks_finished() - value = 1 - yield get_value('test', kwarg='args') - yield _ensure_background_tasks_finished() - # calling thrice be more confident about behaviour of parallel execution - res2 = yield [get_value('test', kwarg='args'), - get_value('test', kwarg='args'), - get_value('test', kwarg='args'), ] - - # then - self.assertEqual(0, res1) - self.assertEqual([1, 1, 1], res2) - - @gen_test - def test_should_return_different_values_on_different_args_with_default_key(self): - # given - value = 0 - - @memoize() - @gen.coroutine - def get_value(arg, kwarg=None): - return value - - # when - res1 = yield get_value('test1', kwarg='args') - time.sleep(.200) - yield _ensure_background_tasks_finished() - value = 1 - res2 = yield get_value('test2', kwarg='args') - - # then - self.assertEqual(0, res1) - self.assertEqual(1, res2) - - @gen_test - def test_should_return_different_values_on_different_kwargs_with_default_key(self): - # given - value = 0 - - @memoize() - @gen.coroutine - def get_value(arg, kwarg=None): - return value - - # when - res1 = yield get_value('test', kwarg='args1') - time.sleep(.200) - yield _ensure_background_tasks_finished() - value = 1 - res2 = yield get_value('test', kwarg='args2') - - # then - self.assertEqual(0, res1) - self.assertEqual(1, res2) - - @gen_test - def test_should_return_same_value_on_constant_key_function(self): - # given - value = 0 - key_extractor = Mock() - key_extractor.format_key = Mock(return_value='lol') - - @memoize( - configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_key_extractor(key_extractor) - ) - @gen.coroutine - def get_value(arg, kwarg=None): - return value - - # when - res1 = yield get_value('test1', kwarg='args1') - time.sleep(.200) - yield _ensure_background_tasks_finished() - value = 1 - res2 = yield get_value('test2', kwarg='args2') - - # then - self.assertEqual(0, res1) - self.assertEqual(0, res2) - - @gen_test - def test_should_release_keys_on_caching_multiple_elements(self): - # given - value = 0 - storage = LocalInMemoryCacheStorage() - key_extractor = Mock() - key_extractor.format_key = Mock(side_effect=lambda method, args, kwargs: str((args[0], kwargs.get('kwarg')))) - - @memoize( - configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_eviction_strategy(LeastRecentlyUpdatedEvictionStrategy(capacity=2)) - .set_key_extractor(key_extractor) - .set_storage(storage) - ) - @gen.coroutine - def get_value(arg, kwarg=None): - return value - - # when - yield get_value('test1', kwarg='args1') - yield get_value('test2', kwarg='args2') - yield get_value('test3', kwarg='args3') - yield get_value('test4', kwarg='args4') - yield _ensure_background_tasks_finished() - - # then - s1 = yield storage.get("('test1', 'args1')") - s2 = yield storage.get("('test2', 'args2')") - s3 = yield storage.get("('test3', 'args3')") - s4 = yield storage.get("('test4', 'args4')") - - self.assertIsNone(s1) - self.assertIsNone(s2) - self.assertIsNotNone(s3) - self.assertIsNotNone(s4) - - @gen_test - def test_should_throw_exception_on_configuration_not_ready(self): - # given - @memoize( - configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_configured(False) - ) - @gen.coroutine - def get_value(arg, kwarg=None): - raise ValueError("Get lost") - - # when - with self.assertRaises(Exception) as context: - yield get_value('test1', kwarg='args1') - - # then - expected = NotConfiguredCacheCalledException() - self.assertEqual(str(expected), str(context.exception)) - - @gen_test - def test_should_throw_exception_on_wrapped_method_failure(self): - # given - @memoize() - @gen.coroutine - def get_value(arg, kwarg=None): - raise ValueError("Get lost") - - # when - with self.assertRaises(Exception) as context: - yield get_value('test1', kwarg='args1') - - # then - self.assertEqual(str(context.exception), str(CachedMethodFailedException('Refresh failed to complete'))) - self.assertEqual(str(context.exception.__cause__), str(ValueError("Get lost"))) - - @gen_test - def test_should_throw_exception_on_refresh_timeout(self): - # given - - @memoize(configuration=DefaultInMemoryCacheConfiguration(method_timeout=timedelta(milliseconds=100))) - @gen.coroutine - def get_value(arg, kwarg=None): - yield _ensure_background_tasks_finished() - time.sleep(.200) - yield _ensure_background_tasks_finished() - return 0 - - # when - with self.assertRaises(Exception) as context: - yield get_value('test1', kwarg='args1') - - # then - self.assertEqual(context.exception.__class__, CachedMethodFailedException) - self.assertEqual(context.exception.__cause__.__class__, _timeout_error_type()) - diff --git a/tests/unit/test_eviction.py b/tests/unit/test_eviction.py index 51f182e..1453f61 100644 --- a/tests/unit/test_eviction.py +++ b/tests/unit/test_eviction.py @@ -1,29 +1,21 @@ -from tests.py310workaround import fix_python_3_10_compatibility - -fix_python_3_10_compatibility() - +import asyncio import time from datetime import timedelta from unittest.mock import Mock -import tornado -from tornado import gen -from tornado.testing import AsyncTestCase, gen_test +import pytest -from tests import _ensure_background_tasks_finished, _assert_called_once_with, AnyObject, _as_future from memoize.configuration import MutableCacheConfiguration, DefaultInMemoryCacheConfiguration from memoize.entrybuilder import ProvidedLifeSpanCacheEntryBuilder from memoize.wrapper import memoize +from tests import _assert_called_once_with, AnyObject, _as_future, _ensure_asyncio_background_tasks_finished, \ + _ensure_background_tasks_finished -class EvictionStrategyInteractionsTests(AsyncTestCase): +@pytest.mark.asyncio +class TestEvictionStrategyInteractions: - def setUp(self): - self.maxDiff = None - super().setUp() - - @gen_test - def test_should_inform_eviction_strategy_on_entry_added(self): + async def test_should_inform_eviction_strategy_on_entry_added(self): # given key_extractor = Mock() key_extractor.format_key = Mock(return_value='key') @@ -32,23 +24,20 @@ def test_should_inform_eviction_strategy_on_entry_added(self): @memoize( configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_key_extractor(key_extractor) - .set_eviction_strategy(eviction_strategy) + .initialized_with(DefaultInMemoryCacheConfiguration()) + .set_key_extractor(key_extractor) + .set_eviction_strategy(eviction_strategy) ) - @gen.coroutine - def sample_method(arg, kwarg=None): + async def sample_method(arg, kwarg=None): return arg, kwarg # when - yield sample_method('test', kwarg='args') - yield _ensure_background_tasks_finished() + await sample_method('test', kwarg='args') # then - _assert_called_once_with(self, eviction_strategy.mark_written, ('key', AnyObject()), {}) + _assert_called_once_with(eviction_strategy.mark_written, ('key', AnyObject()), {}) - @gen_test - def test_should_inform_eviction_strategy_on_entry_updated(self): + async def test_should_inform_eviction_strategy_on_entry_updated(self): # given key_extractor = Mock() key_extractor.format_key = Mock(return_value='key') @@ -57,29 +46,24 @@ def test_should_inform_eviction_strategy_on_entry_updated(self): @memoize( configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_entry_builder(ProvidedLifeSpanCacheEntryBuilder(update_after=timedelta(milliseconds=100))) - .set_key_extractor(key_extractor) - .set_eviction_strategy(eviction_strategy) + .initialized_with(DefaultInMemoryCacheConfiguration()) + .set_entry_builder(ProvidedLifeSpanCacheEntryBuilder(update_after=timedelta(milliseconds=100))) + .set_key_extractor(key_extractor) + .set_eviction_strategy(eviction_strategy) ) - @gen.coroutine - def sample_method(arg, kwarg=None): + async def sample_method(arg, kwarg=None): return arg, kwarg - yield sample_method('test', kwarg='args') - yield _ensure_background_tasks_finished() - time.sleep(.200) - eviction_strategy.mark_written.reset_mock() + await sample_method('test', kwarg='args') + await asyncio.sleep(0.200) # when - yield sample_method('test', kwarg='args') - yield _ensure_background_tasks_finished() + await sample_method('test', kwarg='args') # then - _assert_called_once_with(self, eviction_strategy.mark_written, ('key', AnyObject()), {}) + _assert_called_once_with(eviction_strategy.mark_written, ('key', AnyObject()), {}) - @gen_test - def test_should_inform_eviction_strategy_on_entry_mark_read(self): + async def test_should_inform_eviction_strategy_on_entry_mark_read(self): # given key_extractor = Mock() key_extractor.format_key = Mock(return_value='key') @@ -88,26 +72,22 @@ def test_should_inform_eviction_strategy_on_entry_mark_read(self): @memoize( configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_key_extractor(key_extractor) - .set_eviction_strategy(eviction_strategy) + .initialized_with(DefaultInMemoryCacheConfiguration()) + .set_key_extractor(key_extractor) + .set_eviction_strategy(eviction_strategy) ) - @gen.coroutine - def sample_method(arg, kwarg=None): + async def sample_method(arg, kwarg=None): return arg, kwarg - yield sample_method('test', kwarg='args') - yield _ensure_background_tasks_finished() + await sample_method('test', kwarg='args') # when - yield sample_method('test', kwarg='args') - yield _ensure_background_tasks_finished() + await sample_method('test', kwarg='args') # then eviction_strategy.mark_read.assert_called_once_with('key') - @gen_test - def test_should_inform_eviction_strategy_entry_mark_released(self): + async def test_should_inform_eviction_strategy_entry_mark_released(self): # given key_extractor = Mock() key_extractor.format_key = Mock(side_effect=lambda method, args, kwargs: str((args, kwargs))) @@ -117,23 +97,21 @@ def test_should_inform_eviction_strategy_entry_mark_released(self): @memoize( configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_key_extractor(key_extractor) - .set_eviction_strategy(eviction_strategy) + .initialized_with(DefaultInMemoryCacheConfiguration()) + .set_key_extractor(key_extractor) + .set_eviction_strategy(eviction_strategy) ) - @gen.coroutine - def sample_method(arg, kwarg=None): + async def sample_method(arg, kwarg=None): return arg, kwarg # when - yield sample_method('test', kwarg='args') - yield _ensure_background_tasks_finished() + await sample_method('test', kwarg='args') + await _ensure_background_tasks_finished() # then eviction_strategy.mark_released.assert_called_once_with('release-test') - @gen_test - def test_should_retrieve_entry_to_release_on_entry_added(self): + async def test_should_retrieve_entry_to_release_on_entry_added(self): # given key_extractor = Mock() key_extractor.format_key = Mock(side_effect=lambda method, args, kwargs: str((args, kwargs))) @@ -148,25 +126,23 @@ def test_should_retrieve_entry_to_release_on_entry_added(self): @memoize( configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_storage(storage) - .set_key_extractor(key_extractor) - .set_eviction_strategy(eviction_strategy) + .initialized_with(DefaultInMemoryCacheConfiguration()) + .set_storage(storage) + .set_key_extractor(key_extractor) + .set_eviction_strategy(eviction_strategy) ) - @gen.coroutine - def sample_method(arg, kwarg=None): + async def sample_method(arg, kwarg=None): return arg, kwarg # when - yield sample_method('test', kwarg='args') - yield _ensure_background_tasks_finished() + await sample_method('test', kwarg='args') + await _ensure_background_tasks_finished() # then eviction_strategy.next_to_release.assert_called_once_with() storage.release.assert_called_once_with('release-test') - @gen_test - def test_should_retrieve_entry_to_release_on_entry_updated(self): + async def test_should_retrieve_entry_to_release_on_entry_updated(self): # given key_extractor = Mock() key_extractor.format_key = Mock(side_effect=lambda method, args, kwargs: str((args, kwargs))) @@ -181,25 +157,24 @@ def test_should_retrieve_entry_to_release_on_entry_updated(self): @memoize( configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_key_extractor(key_extractor) - .set_eviction_strategy(eviction_strategy) - .set_storage(storage) - .set_entry_builder(ProvidedLifeSpanCacheEntryBuilder(update_after=timedelta(milliseconds=50))) + .initialized_with(DefaultInMemoryCacheConfiguration()) + .set_key_extractor(key_extractor) + .set_eviction_strategy(eviction_strategy) + .set_storage(storage) + .set_entry_builder(ProvidedLifeSpanCacheEntryBuilder(update_after=timedelta(milliseconds=50))) ) - @gen.coroutine - def sample_method(arg, kwarg=None): + async def sample_method(arg, kwarg=None): return arg, kwarg - yield sample_method('test', kwarg='args') - yield _ensure_background_tasks_finished() + await sample_method('test', kwarg='args') + await _ensure_asyncio_background_tasks_finished() time.sleep(.200) eviction_strategy.next_to_release.reset_mock() storage.release.reset_mock() # when - yield sample_method('test', kwarg='args') - yield _ensure_background_tasks_finished() + await sample_method('test', kwarg='args') + await _ensure_asyncio_background_tasks_finished() # then eviction_strategy.next_to_release.assert_called_once_with() diff --git a/tests/unit/test_invalidation.py b/tests/unit/test_invalidation.py index 420fd9a..82c420f 100644 --- a/tests/unit/test_invalidation.py +++ b/tests/unit/test_invalidation.py @@ -1,48 +1,46 @@ +import pytest + from tests.py310workaround import fix_python_3_10_compatibility fix_python_3_10_compatibility() -from tornado import gen -from tornado.testing import AsyncTestCase, gen_test - from memoize.invalidation import InvalidationSupport from memoize.wrapper import memoize -class TestInvalidationSupport(AsyncTestCase): - @gen_test - def test_invalidation(self): +@pytest.mark.asyncio(scope="class") +class TestInvalidationSupport: + + async def test_invalidation(self): # given invalidation = invalidation = InvalidationSupport() global counter counter = 0 @memoize(invalidation=invalidation) - @gen.coroutine - def sample_method(arg, kwarg=None): + async def sample_method(arg, kwarg=None): global counter counter += 1 return counter # when - res1 = yield sample_method('test', kwarg='args') - res2 = yield sample_method('test', kwarg='args') - yield invalidation.invalidate_for_arguments(('test',), {'kwarg': 'args'}) - res3 = yield sample_method('test', kwarg='args') - res4 = yield sample_method('test', kwarg='args') - yield invalidation.invalidate_for_arguments(('test',), {'kwarg': 'args'}) - res5 = yield sample_method('test', kwarg='args') - # yield _ensure_background_tasks_finished() + res1 = await sample_method('test', kwarg='args') + res2 = await sample_method('test', kwarg='args') + await invalidation.invalidate_for_arguments(('test',), {'kwarg': 'args'}) + res3 = await sample_method('test', kwarg='args') + res4 = await sample_method('test', kwarg='args') + await invalidation.invalidate_for_arguments(('test',), {'kwarg': 'args'}) + res5 = await sample_method('test', kwarg='args') + # await _ensure_background_tasks_finished() # then - self.assertEqual(res1, 1) - self.assertEqual(res2, 1) - self.assertEqual(res3, 2) # post-invalidation - self.assertEqual(res4, 2) # post-invalidation - self.assertEqual(res5, 3) # post-second-invalidation + assert res1 == 1 + assert res2 == 1 + assert res3 == 2 # post-invalidation + assert res4 == 2 # post-invalidation + assert res5 == 3 # post-second-invalidation - @gen_test - def test_invalidation_throws_when_not_configured(self): + async def test_invalidation_throws_when_not_configured(self): # given invalidation = InvalidationSupport() global counter @@ -51,16 +49,16 @@ def test_invalidation_throws_when_not_configured(self): @memoize( # would be properly configured if invalidation would be set via `invalidation=invalidation` ) - @gen.coroutine - def sample_method(arg, kwarg=None): + async def sample_method(arg, kwarg=None): global counter counter += 1 return counter # when - with self.assertRaises(Exception) as context: - yield invalidation.invalidate_for_arguments(('test',), {'kwarg': 'args'}) + with pytest.raises(Exception) as context: + await invalidation.invalidate_for_arguments(('test',), {'kwarg': 'args'}) # then - expected = RuntimeError("Uninitialized: InvalidationSupport should be passed to @memoize to have it initialized") - self.assertEqual(str(context.exception), str(expected)) + expected = RuntimeError( + "Uninitialized: InvalidationSupport should be passed to @memoize to have it initialized") + assert str(context.value) == str(expected) diff --git a/tests/unit/test_key.py b/tests/unit/test_key.py index 0b97e0e..7e3b3fb 100644 --- a/tests/unit/test_key.py +++ b/tests/unit/test_key.py @@ -1,47 +1,43 @@ +import pytest + from tests.py310workaround import fix_python_3_10_compatibility fix_python_3_10_compatibility() from unittest.mock import Mock -import tornado -from tornado import gen -from tornado.testing import AsyncTestCase, gen_test - from tests import _ensure_background_tasks_finished, _assert_called_once_with, AnyObject, _as_future from memoize.configuration import MutableCacheConfiguration, DefaultInMemoryCacheConfiguration from memoize.key import EncodedMethodNameAndArgsKeyExtractor, EncodedMethodReferenceAndArgsKeyExtractor from memoize.wrapper import memoize -class KeyExtractorInteractionsTests(AsyncTestCase): +@pytest.mark.asyncio(scope="class") +class TestKeyExtractorInteractions: - @gen_test - def test_should_call_key_extractor_on_method_used(self): + async def test_should_call_key_extractor_on_method_used(self): # given key_extractor = Mock() key_extractor.format_key = Mock(return_value='key') @memoize( configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_key_extractor(key_extractor) + .initialized_with(DefaultInMemoryCacheConfiguration()) + .set_key_extractor(key_extractor) ) - @gen.coroutine - def sample_method(arg, kwarg=None): + async def sample_method(arg, kwarg=None): return arg, kwarg # when - yield sample_method('test', kwarg='args') - yield _ensure_background_tasks_finished() + await sample_method('test', kwarg='args') + await _ensure_background_tasks_finished() # then # ToDo: assert wrapped methods match somehow - _assert_called_once_with(self, key_extractor.format_key, (AnyObject(), ('test',), {'kwarg': 'args'},), {}) + _assert_called_once_with(key_extractor.format_key, (AnyObject(), ('test',), {'kwarg': 'args'},), {}) - @gen_test - def test_should_pass_extracted_key_to_storage_on_entry_added_to_cache(self): + async def test_should_pass_extracted_key_to_storage_on_entry_added_to_cache(self): # given key_extractor = Mock() key_extractor.format_key = Mock(return_value='key') @@ -52,24 +48,22 @@ def test_should_pass_extracted_key_to_storage_on_entry_added_to_cache(self): @memoize( configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_key_extractor(key_extractor) - .set_storage(storage) + .initialized_with(DefaultInMemoryCacheConfiguration()) + .set_key_extractor(key_extractor) + .set_storage(storage) ) - @gen.coroutine - def sample_method(arg, kwarg=None): + async def sample_method(arg, kwarg=None): return arg, kwarg # when - yield sample_method('test', kwarg='args') - yield _ensure_background_tasks_finished() + await sample_method('test', kwarg='args') + await _ensure_background_tasks_finished() # then storage.get.assert_called_once_with('key') - _assert_called_once_with(self, storage.offer, ('key', AnyObject()), {}) + _assert_called_once_with(storage.offer, ('key', AnyObject()), {}) - @gen_test - def test_should_pass_extracted_key_to_eviction_strategy_on_entry_added_to_cache(self): + async def test_should_pass_extracted_key_to_eviction_strategy_on_entry_added_to_cache(self): # given key_extractor = Mock() key_extractor.format_key = Mock(return_value='key') @@ -78,32 +72,30 @@ def test_should_pass_extracted_key_to_eviction_strategy_on_entry_added_to_cache( @memoize( configuration=MutableCacheConfiguration - .initialized_with(DefaultInMemoryCacheConfiguration()) - .set_eviction_strategy(eviction_strategy) - .set_key_extractor(key_extractor) + .initialized_with(DefaultInMemoryCacheConfiguration()) + .set_eviction_strategy(eviction_strategy) + .set_key_extractor(key_extractor) ) - @gen.coroutine - def sample_method(arg, kwarg=None): + async def sample_method(arg, kwarg=None): return arg, kwarg # when - yield sample_method('test', kwarg='args') - yield _ensure_background_tasks_finished() - yield sample_method('test', kwarg='args') - yield _ensure_background_tasks_finished() + await sample_method('test', kwarg='args') + await _ensure_background_tasks_finished() + await sample_method('test', kwarg='args') + await _ensure_background_tasks_finished() # then eviction_strategy.mark_read.assert_called_once_with('key') - _assert_called_once_with(self, eviction_strategy.mark_written, ('key', AnyObject()), {}) + _assert_called_once_with(eviction_strategy.mark_written, ('key', AnyObject()), {}) -class EncodedMethodReferenceAndArgsKeyExtractorTests(AsyncTestCase): +class EncodedMethodReferenceAndArgsKeyExtractorTests: def helper_method(self, x, y, z='val'): pass - @gen_test - def test_should_format_key(self): + async def test_should_format_key(self): # given key_extractor = EncodedMethodReferenceAndArgsKeyExtractor() @@ -111,18 +103,15 @@ def test_should_format_key(self): key = key_extractor.format_key(self.helper_method, ('a', 'b',), {'z': 'c'}) # then - self.assertEqual(key, "(" + str(self.helper_method) + ", ('a', 'b'), {'z': 'c'})") + assert key, "(" + str(self.helper_method) + ", ('a', 'b') == {'z': 'c'})" -class EncodedMethodNameAndArgsKeyExtractorTests(AsyncTestCase): - def get_new_ioloop(self): - return tornado.platform.asyncio.AsyncIOMainLoop() +class EncodedMethodNameAndArgsKeyExtractorTests: def helper_method(self, x, y, z='val'): pass - @gen_test - def test_should_format_key_with_all_args_on_skip_flag_not_set(self): + async def test_should_format_key_with_all_args_on_skip_flag_not_set(self): # given key_extractor = EncodedMethodNameAndArgsKeyExtractor(skip_first_arg_as_self=False) @@ -130,10 +119,9 @@ def test_should_format_key_with_all_args_on_skip_flag_not_set(self): key = key_extractor.format_key(self.helper_method, ('a', 'b',), {'z': 'c'}) # then - self.assertEqual(key, "('helper_method', ('a', 'b'), {'z': 'c'})") + assert key, "('helper_method', ('a', 'b') == {'z': 'c'})" - @gen_test - def test_should_format_key_with_all_args_on_skip_flag_set(self): + async def test_should_format_key_with_all_args_on_skip_flag_set(self): # given key_extractor = EncodedMethodNameAndArgsKeyExtractor(skip_first_arg_as_self=True) @@ -141,4 +129,4 @@ def test_should_format_key_with_all_args_on_skip_flag_set(self): key = key_extractor.format_key(self.helper_method, (self, 'a', 'b',), {'z': 'c'}) # then - self.assertEqual(key, "('helper_method', ('a', 'b'), {'z': 'c'})") + assert key, "('helper_method', ('a', 'b') == {'z': 'c'})" diff --git a/tests/unit/test_postprocessing.py b/tests/unit/test_postprocessing.py index 0b0888e..8a4c24a 100644 --- a/tests/unit/test_postprocessing.py +++ b/tests/unit/test_postprocessing.py @@ -1,3 +1,5 @@ +import pytest + from memoize.postprocessing import DeepcopyPostprocessing from tests.py310workaround import fix_python_3_10_compatibility @@ -5,17 +7,14 @@ from unittest.mock import Mock -from tornado import gen -from tornado.testing import AsyncTestCase, gen_test - from memoize.configuration import MutableCacheConfiguration, DefaultInMemoryCacheConfiguration from memoize.wrapper import memoize -class KeyExtractorInteractionsTests(AsyncTestCase): +@pytest.mark.asyncio(scope="class") +class TestKeyExtractorInteractions: - @gen_test - def test_postprocessing_is_applied(self): + async def test_postprocessing_is_applied(self): # given postprocessing = Mock() postprocessing.apply = Mock(return_value='overridden-by-postprocessing') @@ -25,20 +24,18 @@ def test_postprocessing_is_applied(self): .initialized_with(DefaultInMemoryCacheConfiguration()) .set_postprocessing(postprocessing) ) - @gen.coroutine - def sample_method(arg): + async def sample_method(arg): return f"value-for-{arg}" # when - result = yield sample_method('test') + result = await sample_method('test') # then postprocessing.apply.assert_called_once() postprocessing.apply.assert_called_once_with('value-for-test') - self.assertEqual(result, 'overridden-by-postprocessing') + assert result == 'overridden-by-postprocessing' - @gen_test - def test_postprocessing_based_on_deepcopy_prevents_modifying_value_cached_in_memory(self): + async def test_postprocessing_based_on_deepcopy_prevents_modifying_value_cached_in_memory(self): # given @memoize( @@ -46,15 +43,14 @@ def test_postprocessing_based_on_deepcopy_prevents_modifying_value_cached_in_mem .initialized_with(DefaultInMemoryCacheConfiguration()) .set_postprocessing(DeepcopyPostprocessing()) ) - @gen.coroutine - def sample_method(arg): + async def sample_method(arg): return {'arg': arg, 'list': [4, 5, 1, 2, 3]} # unsorted # when - result1 = yield sample_method('test') - result2 = yield sample_method('test') + result1 = await sample_method('test') + result2 = await sample_method('test') result1['list'].sort() # then - self.assertEqual(result1, {'arg': 'test', 'list': [1, 2, 3, 4, 5]}) # sorted in-place - self.assertEqual(result2, {'arg': 'test', 'list': [4, 5, 1, 2, 3]}) # still unsorted + assert result1, {'arg': 'test', 'list': [1, 2, 3, 4 == 5]} # sorted in-place + assert result2, {'arg': 'test', 'list': [4, 5, 1, 2 == 3]} # still unsorted diff --git a/tests/unit/test_serde.py b/tests/unit/test_serde.py index 29d5a92..c5d8ad0 100644 --- a/tests/unit/test_serde.py +++ b/tests/unit/test_serde.py @@ -1,3 +1,5 @@ +import pytest + from tests.py310workaround import fix_python_3_10_compatibility fix_python_3_10_compatibility() @@ -9,15 +11,14 @@ from pickle import HIGHEST_PROTOCOL, DEFAULT_PROTOCOL from unittest.mock import Mock -from tornado.testing import AsyncTestCase, gen_test - from memoize.entry import CacheEntry from memoize.serde import PickleSerDe, EncodingSerDe, JsonSerDe -class EncodingSerDeTests(AsyncTestCase): - @gen_test - def test_should_apply_encoding_on_wrapped_serde_results(self): +@pytest.mark.asyncio(scope="class") +class TestEncodingSerDe: + + async def test_should_apply_encoding_on_wrapped_serde_results(self): # given cache_entry = CacheEntry(datetime.now(), datetime.now(), datetime.now(), "value") serde = Mock() @@ -29,11 +30,10 @@ def test_should_apply_encoding_on_wrapped_serde_results(self): # then expected = codecs.encode(b"in", 'base64') - self.assertEqual(bytes, expected) + assert bytes == expected serde.serialize.assert_called_once_with(cache_entry) - @gen_test - def test_should_apply_decoding_on_wrapped_serde_results(self): + async def test_should_apply_decoding_on_wrapped_serde_results(self): # given cache_entry = CacheEntry(datetime.now(), datetime.now(), datetime.now(), "value") encoded_cache_entry = codecs.encode(b'y', 'base64') @@ -45,11 +45,10 @@ def test_should_apply_decoding_on_wrapped_serde_results(self): data = wrapper.deserialize(encoded_cache_entry) # then - self.assertEqual(data, cache_entry) + assert data == cache_entry serde.deserialize.assert_called_once_with(b'y') - @gen_test - def test_e2e_integration_with_sample_serde(self): + async def test_e2e_integration_with_sample_serde(self): # given cache_entry = CacheEntry(datetime.now(), datetime.now(), datetime.now(), "value") serde = PickleSerDe(pickle_protocol=HIGHEST_PROTOCOL) # sample serde @@ -60,12 +59,12 @@ def test_e2e_integration_with_sample_serde(self): data = wrapper.deserialize(encoded_cache_entry) # then - self.assertEqual(data, cache_entry) + assert data == cache_entry -class JsonSerDeTests(AsyncTestCase): - @gen_test - def test_should_encode_as_readable_json(self): +class JsonSerDeTests: + + async def test_should_encode_as_readable_json(self): # given cache_entry = CacheEntry(datetime.fromtimestamp(1, timezone.utc), datetime.fromtimestamp(2, timezone.utc), datetime.fromtimestamp(3, timezone.utc), "in") @@ -76,13 +75,12 @@ def test_should_encode_as_readable_json(self): # then parsed = json.loads(codecs.decode(bytes)) # verified this way due to json/ujson slight separator differences - self.assertEqual(parsed, {"created": cache_entry.created.timestamp(), - "update_after": cache_entry.update_after.timestamp(), - "expires_after": cache_entry.expires_after.timestamp(), - "value": "in"}) + assert parsed == {"created": cache_entry.created.timestamp(), + "update_after": cache_entry.update_after.timestamp(), + "expires_after": cache_entry.expires_after.timestamp(), + "value": "in"} - @gen_test - def test_should_decode_readable_json(self): + async def test_should_decode_readable_json(self): # given serde = JsonSerDe(string_encoding='utf-8') @@ -92,10 +90,9 @@ def test_should_decode_readable_json(self): # then cache_entry = CacheEntry(datetime.fromtimestamp(1, timezone.utc), datetime.fromtimestamp(2, timezone.utc), datetime.fromtimestamp(3, timezone.utc), "value") - self.assertEqual(bytes, cache_entry) + assert bytes == cache_entry - @gen_test - def test_should_apply_value_transformations_on_serialization(self): + async def test_should_apply_value_transformations_on_serialization(self): # given cache_entry = CacheEntry(datetime.fromtimestamp(1, timezone.utc), datetime.fromtimestamp(2, timezone.utc), datetime.fromtimestamp(3, timezone.utc), "in") @@ -108,14 +105,13 @@ def test_should_apply_value_transformations_on_serialization(self): # then parsed = json.loads(codecs.decode(bytes)) # verified this way due to json/ujson slight separator differences - self.assertEqual(parsed, {"created": cache_entry.created.timestamp(), - "update_after": cache_entry.update_after.timestamp(), - "expires_after": cache_entry.expires_after.timestamp(), - "value": "out"}) + assert parsed == {"created": cache_entry.created.timestamp(), + "update_after": cache_entry.update_after.timestamp(), + "expires_after": cache_entry.expires_after.timestamp(), + "value": "out"} encode.assert_called_once_with("in") - @gen_test - def test_should_apply_value_transformations_on_deserialization(self): + async def test_should_apply_value_transformations_on_deserialization(self): # given encode = Mock(return_value="in") decode = Mock(return_value="out") @@ -127,13 +123,13 @@ def test_should_apply_value_transformations_on_deserialization(self): # then cache_entry = CacheEntry(datetime.fromtimestamp(1, timezone.utc), datetime.fromtimestamp(2, timezone.utc), datetime.fromtimestamp(3, timezone.utc), "out") - self.assertEqual(data, cache_entry) + assert data == cache_entry decode.assert_called_once_with("in") -class PickleSerDeTests(AsyncTestCase): - @gen_test - def test_should_pickle_using_highest_protocol(self): +class PickleSerDeTests: + + async def test_should_pickle_using_highest_protocol(self): # given cache_entry = CacheEntry(datetime.now(), datetime.now(), datetime.now(), "value") serde = PickleSerDe(pickle_protocol=HIGHEST_PROTOCOL) @@ -143,10 +139,9 @@ def test_should_pickle_using_highest_protocol(self): # then expected = pickle.dumps(cache_entry, protocol=HIGHEST_PROTOCOL) - self.assertEqual(bytes, expected) + assert bytes == expected - @gen_test - def test_should_unpickle_using_highest_protocol(self): + async def test_should_unpickle_using_highest_protocol(self): # given cache_entry = CacheEntry(datetime.now(), datetime.now(), datetime.now(), "value") serde = PickleSerDe(pickle_protocol=HIGHEST_PROTOCOL) @@ -156,10 +151,9 @@ def test_should_unpickle_using_highest_protocol(self): data = serde.deserialize(bytes) # then - self.assertEqual(data, cache_entry) + assert data == cache_entry - @gen_test - def test_should_pickle_using_default_protocol(self): + async def test_should_pickle_using_default_protocol(self): # given cache_entry = CacheEntry(datetime.now(), datetime.now(), datetime.now(), "value") serde = PickleSerDe(pickle_protocol=DEFAULT_PROTOCOL) @@ -169,10 +163,9 @@ def test_should_pickle_using_default_protocol(self): # then expected = pickle.dumps(cache_entry, protocol=DEFAULT_PROTOCOL) - self.assertEqual(bytes, expected) + assert bytes == expected - @gen_test - def test_should_unpickle_using_default_protocol(self): + async def test_should_unpickle_using_default_protocol(self): # given cache_entry = CacheEntry(datetime.now(), datetime.now(), datetime.now(), "value") serde = PickleSerDe(pickle_protocol=DEFAULT_PROTOCOL) @@ -182,4 +175,4 @@ def test_should_unpickle_using_default_protocol(self): data = serde.deserialize(bytes) # then - self.assertEqual(data, cache_entry) + assert data == cache_entry diff --git a/tests/unit/test_statuses.py b/tests/unit/test_statuses.py index 71c76a4..cac0ab0 100644 --- a/tests/unit/test_statuses.py +++ b/tests/unit/test_statuses.py @@ -1,55 +1,52 @@ +import pytest + from tests.py310workaround import fix_python_3_10_compatibility fix_python_3_10_compatibility() from datetime import timedelta -from tornado.testing import AsyncTestCase, gen_test - from memoize.statuses import UpdateStatuses -class UpdateStatusesTests(AsyncTestCase): +@pytest.mark.asyncio(scope="class") +class TestStatuses: - def setUp(self): - super().setUp() + def setup_method(self): self.update_statuses = UpdateStatuses() - def tearDown(self): - super().tearDown() - - def test_should_not_be_updating(self): + async def test_should_not_be_updating(self): # given/when/then - self.assertFalse(self.update_statuses.mark_being_updated('key')) + assert not self.update_statuses.mark_being_updated('key') - def test_should_be_updating(self): + async def test_should_be_updating(self): # given/when self.update_statuses.mark_being_updated('key') # then - self.assertTrue(self.update_statuses.is_being_updated('key')) + assert self.update_statuses.is_being_updated('key') - def test_should_raise_exception_during_marked_as_being_updated(self): + async def test_should_raise_exception_during_marked_as_being_updated(self): # given self.update_statuses.mark_being_updated('key') # when/then - with self.assertRaises(ValueError): + with pytest.raises(ValueError): self.update_statuses.mark_being_updated('key') - def test_should_be_marked_as_being_updated(self): + async def test_should_be_marked_as_being_updated(self): # given/when self.update_statuses.mark_being_updated('key') # then - self.assertTrue(self.update_statuses.is_being_updated('key')) + assert self.update_statuses.is_being_updated('key') - def test_should_raise_exception_during_be_mark_as_updated(self): + async def test_should_raise_exception_during_be_mark_as_updated(self): # given/when/then - with self.assertRaises(ValueError): + with pytest.raises(ValueError): self.update_statuses.mark_updated('key', None) - def test_should_be_mark_as_updated(self): + async def test_should_be_mark_as_updated(self): # given self.update_statuses.mark_being_updated('key') @@ -57,14 +54,14 @@ def test_should_be_mark_as_updated(self): self.update_statuses.mark_updated('key', 'entry') # then - self.assertFalse(self.update_statuses.is_being_updated('key')) + assert not self.update_statuses.is_being_updated('key') - def test_should_raise_exception_during_mark_update_as_aborted(self): + async def test_should_raise_exception_during_mark_update_as_aborted(self): # given/when/then - with self.assertRaises(ValueError): + with pytest.raises(ValueError): self.update_statuses.mark_update_aborted('key', Exception('stub')) - def test_should_mark_update_as_aborted(self): + async def test_should_mark_update_as_aborted(self): # given self.update_statuses.mark_being_updated('key') @@ -72,15 +69,13 @@ def test_should_mark_update_as_aborted(self): self.update_statuses.mark_update_aborted('key', Exception('stub')) # then - self.assertFalse(self.update_statuses.is_being_updated('key')) + assert not self.update_statuses.is_being_updated('key') - @gen_test - def test_should_raise_exception_during_await_updated(self): + async def test_should_raise_exception_during_await_updated(self): # given/when/then - with self.assertRaises(ValueError): - yield self.update_statuses.await_updated('key') + with pytest.raises(ValueError): + await self.update_statuses.await_updated('key') - @gen_test async def test_should_raise_timeout_exception_during_await_updated(self): # given self.update_statuses._update_lock_timeout = timedelta(milliseconds=1) @@ -90,9 +85,8 @@ async def test_should_raise_timeout_exception_during_await_updated(self): await self.update_statuses.await_updated('key') # then - self.assertFalse(self.update_statuses.is_being_updated('key')) + assert not self.update_statuses.is_being_updated('key') - @gen_test async def test_should_await_updated_return_entry(self): # given self.update_statuses.mark_being_updated('key') @@ -103,10 +97,9 @@ async def test_should_await_updated_return_entry(self): result = await result # then - self.assertIsNone(result) - self.assertFalse(self.update_statuses.is_being_updated('key')) + assert result == None + assert not self.update_statuses.is_being_updated('key') - @gen_test async def test_concurrent_callers_should_all_get_exception_on_aborted_update(self): # given self.update_statuses.mark_being_updated('key') @@ -121,7 +114,7 @@ async def test_concurrent_callers_should_all_get_exception_on_aborted_update(sel result3 = await result3 # then - self.assertFalse(self.update_statuses.is_being_updated('key')) - self.assertEqual(str(result1), str(ValueError('stub'))) - self.assertEqual(str(result2), str(ValueError('stub'))) - self.assertEqual(str(result3), str(ValueError('stub'))) + assert not self.update_statuses.is_being_updated('key') + assert str(result1) == str(ValueError('stub')) + assert str(result2) == str(ValueError('stub')) + assert str(result3) == str(ValueError('stub')) diff --git a/tests/unit/test_storage.py b/tests/unit/test_storage.py index c940559..bc5cf1f 100644 --- a/tests/unit/test_storage.py +++ b/tests/unit/test_storage.py @@ -1,9 +1,11 @@ +import pytest + from tests.py310workaround import fix_python_3_10_compatibility fix_python_3_10_compatibility() from datetime import datetime -from tornado.testing import AsyncTestCase, gen_test + from memoize.entry import CacheKey, CacheEntry from memoize.storage import LocalInMemoryCacheStorage @@ -12,41 +14,35 @@ CACHE_KEY = CacheKey("key") -class LocalInMemoryCacheStorageTests(AsyncTestCase): - def setUp(self): - super().setUp() +@pytest.mark.asyncio(scope="class") +class TestLocalInMemoryCacheStorage: + def setup_method(self): self.storage = LocalInMemoryCacheStorage() - def tearDown(self): - super().tearDown() - - @gen_test - def test_offer_and_get_returns_same_object(self): + async def test_offer_and_get_returns_same_object(self): # given - yield self.storage.offer(CACHE_KEY, CACHE_SAMPLE_ENTRY) + await self.storage.offer(CACHE_KEY, CACHE_SAMPLE_ENTRY) # when - returned_value = yield self.storage.get(CACHE_KEY) + returned_value = await self.storage.get(CACHE_KEY) # then - self.assertEqual(returned_value.value, "value") + assert returned_value.value == "value" - @gen_test - def test_get_without_offer_returns_none(self): + async def test_get_without_offer_returns_none(self): # given/when - returned_value = yield self.storage.get(CACHE_KEY) + returned_value = await self.storage.get(CACHE_KEY) # then - self.assertIsNone(returned_value) + assert returned_value == None - @gen_test - def test_released_object_is_not_returned(self): + async def test_released_object_is_not_returned(self): # given - yield self.storage.offer(CACHE_KEY, CACHE_SAMPLE_ENTRY) - yield self.storage.release(CACHE_KEY) + await self.storage.offer(CACHE_KEY, CACHE_SAMPLE_ENTRY) + await self.storage.release(CACHE_KEY) # when - returned_value = yield self.storage.get(CACHE_KEY) + returned_value = await self.storage.get(CACHE_KEY) # then - self.assertIsNone(returned_value) + assert returned_value == None diff --git a/tox.ini b/tox.ini index edc339b..3cc3977 100644 --- a/tox.ini +++ b/tox.ini @@ -1,30 +1,35 @@ [tox] skipdist = True - -# py-{asyncio,tornado} are added for GitHub Actions (where we have only one interpreter at the time) -# py{35,36,37,38,39}-{asyncio,tornado} are added for development purposes (where one has multiple interpreters) -envlist = py-{asyncio,tornado},py{37,38,39,310,311,312}-{asyncio,tornado},coverage-py310,mypy-py310 +envlist = py{37,38,39,310,311,312},mypy,coverage [testenv] -setenv = - asyncio: MEMOIZE_FORCE_ASYNCIO=1 - tornado: MEMOIZE_FORCE_ASYNCIO= - coverage: MEMOIZE_FORCE_ASYNCIO= - mypy: MEMOIZE_FORCE_ASYNCIO= +allowlist_externals = pytest +commands = + py{37,38,39,310,311,312}: pytest {posargs:-vv} + coverage: pytest --cov=memoize {posargs:-vv} + mypy: mypy memoize -commands = - asyncio: python setup.py test -q -s tests.asynciotests - tornado: python setup.py test -q -s tests - coverage-py37: coverage run --branch --append --source="memoize" setup.py test - coverage-py37: coverage report - mypy-py37: mypy memoize deps = - setuptools # for setup.py to work (distutils is removed from Python 3.12) - tornado: backports.ssl-match-hostname # for tornado-based tests to run with Python 3.12 - asyncio: backports.ssl-match-hostname # unit tests that run for asyncio are still based on tornado.testing - asyncio: tornado>4,<5 - tornado: tornado>4,<5 - coverage: coverage - coverage: tornado>4,<5 - mypy: mypy - mypy: types-tornado + py{37,38,39,310,311,312}: pytest-asyncio + coverage: pytest-asyncio + coverage: pytest-cov + mypy: mypy + mypy: types-ujson + +[testenv:report] +skip_install = true +deps = coverage +commands = + coverage combine + coverage html + coverage report --fail-under=100 + + +[gh-actions] +python = + 3.7: py37 + 3.8: py38 + 3.9: py39 + 3.10: py310,mypy,coverage + 3.11: py311 + 3.12: py312 \ No newline at end of file