From 8c775d86aff76a2e9e51b8e9e992a92aaa17a499 Mon Sep 17 00:00:00 2001 From: jeremy Date: Thu, 22 Sep 2022 18:13:31 -0400 Subject: [PATCH 1/5] fixed pytest-xprocess import --- tests/conftest.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b24d5795..00a0fe33 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,13 +7,15 @@ import flask_caching as fsc try: - __import__("pytest_xprocess") from xprocess import ProcessStarter except ImportError: + try: + __import__("pytest_xprocess") + except ImportError: - @pytest.fixture(scope="session") - def xprocess(): - pytest.skip("pytest-xprocess not installed.") + @pytest.fixture(scope="session") + def xprocess(): + pytest.skip("pytest-xprocess not installed.") @pytest.fixture From 4a9783556ecd0b9de230575f5fd1eee73748fc55 Mon Sep 17 00:00:00 2001 From: jeremy Date: Tue, 27 Sep 2022 15:36:00 -0400 Subject: [PATCH 2/5] modified timeout to be callable and added related tests --- src/flask_caching/__init__.py | 22 +++++++++++++----- tests/test_memoize.py | 22 ++++++++++++++++++ tests/test_view.py | 43 ++++++++++++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/src/flask_caching/__init__.py b/src/flask_caching/__init__.py index 5d458d19..8a9e395e 100644 --- a/src/flask_caching/__init__.py +++ b/src/flask_caching/__init__.py @@ -236,7 +236,7 @@ def unlink(self, *args, **kwargs) -> bool: def cached( self, - timeout: Optional[int] = None, + timeout: Optional[Union[int, Callable]] = None, key_prefix: str = "view/%s", unless: Optional[Callable] = None, forced_update: Optional[Callable] = None, @@ -406,14 +406,17 @@ def decorated_function(*args, **kwargs): rv = [val for val in rv] if response_filter is None or response_filter(rv): - cache_timeout = decorated_function.cache_timeout + timeout = decorated_function.cache_timeout if isinstance(rv, CachedResponse): - cache_timeout = rv.timeout or cache_timeout + timeout = rv.timeout or timeout + elif callable(timeout): + timeout = timeout(rv) + try: self.cache.set( cache_key, rv, - timeout=cache_timeout, + timeout=timeout, ) except Exception: if self.app.debug: @@ -592,6 +595,9 @@ def _memoize_make_cache_key( def make_cache_key(f, *args, **kwargs): _timeout = getattr(timeout, "cache_timeout", timeout) + if callable(_timeout): + _timeout = 0 # placeholder until timeout(rv) is doable + fname, version_data = self._memoize_version( f, args=args, @@ -724,7 +730,7 @@ def _bypass_cache( def memoize( self, - timeout: Optional[int] = None, + timeout: Optional[Union[int, Callable]] = None, make_name: Optional[Callable] = None, unless: Optional[Callable] = None, forced_update: Optional[Callable] = None, @@ -878,11 +884,15 @@ def decorated_function(*args, **kwargs): rv = [val for val in rv] if response_filter is None or response_filter(rv): + timeout = decorated_function.cache_timeout + if callable(timeout): + timeout = timeout(rv) + try: self.cache.set( cache_key, rv, - timeout=decorated_function.cache_timeout, + timeout=timeout, ) except Exception: if self.app.debug: diff --git a/tests/test_memoize.py b/tests/test_memoize.py index 12465f19..1c2c32fc 100644 --- a/tests/test_memoize.py +++ b/tests/test_memoize.py @@ -73,6 +73,28 @@ def big_foo(a, b): assert big_foo(5, 2) != result +def test_memoize_dynamic_timeout_via_callable_timeout(app): + app.config["CACHE_DEFAULT_TIMEOUT"] = 1 + cache = Cache(app) + + with app.test_request_context(): + @cache.memoize( + # This should override the timeout to be 2 seconds + timeout=lambda rv: 2 if isinstance(rv, int) else 1 + ) + def big_foo(a, b): + return a + b + random.randrange(0, 100000) + + result = big_foo(5, 2) + assert big_foo(5, 2) == result + + time.sleep(1) # after 1 second, cache is still active + assert big_foo(5, 2) == result + + time.sleep(1) # after 2 seconds, cache is not still active + assert big_foo(5, 2) != result + + def test_memoize_annotated(app, cache): with app.test_request_context(): diff --git a/tests/test_view.py b/tests/test_view.py index 99ad77eb..d6ada3ee 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -3,6 +3,7 @@ from flask import make_response from flask import request +from flask import Response from flask.views import View from flask_caching import CachedResponse @@ -280,7 +281,7 @@ def cached_view2(foo, bar): assert time2 != tc.get("/a/b").data.decode("utf-8") -def test_cache_timeout_dynamic(app, cache): +def test_cache_timeout_dynamic_via_cached_reponse(app, cache): @app.route("/") @cache.cached(timeout=1) def cached_view(): @@ -300,6 +301,46 @@ def cached_view(): assert time1 != tc.get("/").data.decode("utf-8") +def test_cache_memoize_timeout_dynamic_via_callable_timeout(app, cache): + @app.route("/") + @cache.memoize( + # This should override the timeout to be 2 seconds + timeout=lambda rv: 2 if isinstance(rv, Response) else 1 + ) + def cached_view(): + return make_response(str(time.time())) + + tc = app.test_client() + rv1 = tc.get("/") + time1 = rv1.data.decode("utf-8") + + time.sleep(1) # after 1 second, cache is still active + assert time1 == tc.get("/").data.decode("utf-8") + + time.sleep(1) # after 2 seconds, cache is not still active + assert time1 != tc.get("/").data.decode("utf-8") + + +def test_cache_cached_reponse_overrides_callable_timeout(app, cache): + @app.route("/") + @cache.cached( + timeout=lambda rv: 1 # second, to be be overridden by CachedResponse + ) + def cached_view(): + # This should override the timeout to be 2 seconds + return CachedResponse(response=make_response(str(time.time())), timeout=2) + + tc = app.test_client() + rv1 = tc.get("/") + time1 = rv1.data.decode("utf-8") + + time.sleep(1) # after 1 second, cache is still active + assert time1 == tc.get("/").data.decode("utf-8") + + time.sleep(1) # after 2 seconds, cache is not still active + assert time1 != tc.get("/").data.decode("utf-8") + + def test_generate_cache_key_from_query_string(app, cache): """Test the _make_cache_key_query_string() cache key maker. From 093c5c8d2ebfdd1e734bb4aa6ca071d885c132dc Mon Sep 17 00:00:00 2001 From: jeremy Date: Tue, 27 Sep 2022 16:18:56 -0400 Subject: [PATCH 3/5] updated docstrings and changelog re callable timeout --- CHANGES.rst | 6 ++++++ src/flask_caching/__init__.py | 18 ++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index a7810971..36008333 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,12 @@ Changelog ========= +Version 2.0.3 +------------- + +- added support for callable `timeout` in `@cached` and `@memoized` +- modified try-except import strategy re pytest-xprocess + Version 2.0.2 ------------- diff --git a/src/flask_caching/__init__.py b/src/flask_caching/__init__.py index 8a9e395e..6b840018 100644 --- a/src/flask_caching/__init__.py +++ b/src/flask_caching/__init__.py @@ -288,8 +288,13 @@ def get_list(): readable and writable - :param timeout: Default None. If set to an integer, will cache for that - amount of time. Unit of time is in seconds. + :param timeout: Default None. If set to an integer or a callable + which returns and integer, it will cache for + that amount of time. As a callable it will be + invoked after the cached function evaluation + and should expect the value from that as its + one argument. The callable's return type should + be None or int. Unit of time is in seconds. :param key_prefix: Default 'view/%(request.path)s'. Beginning key to . use for the cache key. `request.path` will be the @@ -781,8 +786,13 @@ def big_foo(a, b): readable and writable - :param timeout: Default None. If set to an integer, will cache for that - amount of time. Unit of time is in seconds. + :param timeout: Default None. If set to an integer or a callable + which returns and integer, it will cache for + that amount of time. As a callable it will be + invoked after the cached function evaluation + and should expect the value from that as its + one argument. The callable's return type should + be None or int. Unit of time is in seconds. :param make_name: Default None. If set this is a function that accepts a single argument, the function name, and returns a new string to be used as the function name. From b5adf0261898378d01d2d3059092e7fc31544e83 Mon Sep 17 00:00:00 2001 From: jeremy Date: Tue, 27 Sep 2022 17:06:39 -0400 Subject: [PATCH 4/5] corrected docstrings re timeout per CONTRIBUTING.rst. added spaces. --- CHANGES.rst | 2 ++ src/flask_caching/__init__.py | 34 ++++++++++++++++++++-------------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 36008333..36fcd55b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,11 +7,13 @@ Version 2.0.3 - added support for callable `timeout` in `@cached` and `@memoized` - modified try-except import strategy re pytest-xprocess + Version 2.0.2 ------------- - migrate ``flask_caching.backends.RedisCluster`` dependency from redis-py-cluster to redis-py + Version 2.0.1 ------------- diff --git a/src/flask_caching/__init__.py b/src/flask_caching/__init__.py index 6b840018..ce7bcaa7 100644 --- a/src/flask_caching/__init__.py +++ b/src/flask_caching/__init__.py @@ -288,13 +288,13 @@ def get_list(): readable and writable - :param timeout: Default None. If set to an integer or a callable - which returns and integer, it will cache for - that amount of time. As a callable it will be - invoked after the cached function evaluation - and should expect the value from that as its - one argument. The callable's return type should - be None or int. Unit of time is in seconds. + :param timeout: Default None. If set to an integer, will cache for that + amount of time. Unit of time is in seconds. + + .. versionchanged:: 2.0.3 + Can optionally be a callable which expects one + argument, the result of the cached function + evaluation, and returns None or an integer. :param key_prefix: Default 'view/%(request.path)s'. Beginning key to . use for the cache key. `request.path` will be the @@ -786,24 +786,28 @@ def big_foo(a, b): readable and writable - :param timeout: Default None. If set to an integer or a callable - which returns and integer, it will cache for - that amount of time. As a callable it will be - invoked after the cached function evaluation - and should expect the value from that as its - one argument. The callable's return type should - be None or int. Unit of time is in seconds. + :param timeout: Default None. If set to an integer, will cache for that + amount of time. Unit of time is in seconds. + + .. versionchanged:: 2.0.3 + Can optionally be a callable which expects one + argument, the result of the cached function + evaluation, and returns None or an integer. + :param make_name: Default None. If set this is a function that accepts a single argument, the function name, and returns a new string to be used as the function name. If not set then the function name is used. + :param unless: Default None. Cache will *always* execute the caching facilities unless this callable is true. This will bypass the caching entirely. + :param forced_update: Default None. If this callable is true, cache value will be updated regardless cache is expired or not. Useful for background renewal of cached functions. + :param response_filter: Default None. If not None, the callable is invoked after the cached funtion evaluation, and is given one arguement, the response @@ -812,6 +816,7 @@ def big_foo(a, b): caching of code 500 responses. :param hash_method: Default hashlib.md5. The hash method used to generate the keys for cached results. + :param cache_none: Default False. If set to True, add a key exists check when cache.get returns None. This will likely lead to wrongly returned None values in concurrent @@ -826,6 +831,7 @@ def big_foo(a, b): formed with the function's source code hash in addition to other parameters that may be included in the formation of the key. + :param args_to_ignore: List of arguments that will be ignored while generating the cache key. Default to None. This means that those arguments may change From c33ecbb08e0d632780b0b2637b6818571ac318a8 Mon Sep 17 00:00:00 2001 From: jeremy Date: Tue, 27 Sep 2022 18:05:01 -0400 Subject: [PATCH 5/5] fixed formatting per black. updated docs/index.rst. --- docs/index.rst | 10 ++++++++++ tests/test_memoize.py | 5 ++++- tests/test_view.py | 8 ++++---- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 946e5688..0738c98e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -87,6 +87,11 @@ a subclass of `flask.Response`:: timeout=50, ) +.. versionchanged:: 2.0.3 + A dynamic timeout can also be achieved via setting ``@cached``'s `timeout` argument + to a callable which takes the decorated function's output as a positional argument + and returns `None` or an integer. Callable timeout is also available to ``@memoized``. + .. warning:: When using ``cached`` on a view, take care to put it between Flask's @@ -179,6 +184,11 @@ every time this information is needed you might do something like the following: def __repr__(self): return "%s(%s)" % (self.__class__.__name__, self.id) +.. versionchanged:: 2.0.3 + A dynamic timeout can be achieved via setting ``@memoized``'s `timeout` argument + to a callable which takes the decorated function's output as a positional argument + and returns `None` or an integer. Callable timeout is also available to ``@cached``. + Deleting memoize cache `````````````````````` diff --git a/tests/test_memoize.py b/tests/test_memoize.py index 1c2c32fc..cc31a2fe 100644 --- a/tests/test_memoize.py +++ b/tests/test_memoize.py @@ -78,9 +78,12 @@ def test_memoize_dynamic_timeout_via_callable_timeout(app): cache = Cache(app) with app.test_request_context(): + @cache.memoize( # This should override the timeout to be 2 seconds - timeout=lambda rv: 2 if isinstance(rv, int) else 1 + timeout=lambda rv: 2 + if isinstance(rv, int) + else 1 ) def big_foo(a, b): return a + b + random.randrange(0, 100000) diff --git a/tests/test_view.py b/tests/test_view.py index d6ada3ee..5cc9e16b 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -305,7 +305,9 @@ def test_cache_memoize_timeout_dynamic_via_callable_timeout(app, cache): @app.route("/") @cache.memoize( # This should override the timeout to be 2 seconds - timeout=lambda rv: 2 if isinstance(rv, Response) else 1 + timeout=lambda rv: 2 + if isinstance(rv, Response) + else 1 ) def cached_view(): return make_response(str(time.time())) @@ -323,9 +325,7 @@ def cached_view(): def test_cache_cached_reponse_overrides_callable_timeout(app, cache): @app.route("/") - @cache.cached( - timeout=lambda rv: 1 # second, to be be overridden by CachedResponse - ) + @cache.cached(timeout=lambda rv: 1) # timeout to be be overridden by CachedResponse def cached_view(): # This should override the timeout to be 2 seconds return CachedResponse(response=make_response(str(time.time())), timeout=2)