Repository: rotki/rotki

Issue:
Take arguments into account for cache_response_timewise
## Problem Definition

The [cache_response_timewise](https://github.com/rotki/rotki/blob/b1f73ea932935ea801820824e08880b3d19b2bbc/rotkehlchen/utils/misc.py#L83) decorator is caching results of functions independent of the arguments they take.

This is a bug as calling a function with different arguments can return the cached result of the previous arguments which would be wrong.

## Task

- Fix this by taking into account argument values in the cache.
- Test this by re-enabling cache for some of the tests which call cachable functions with different arguments.


Discussion:

Resolving patch:
diff --git a/rotkehlchen/utils/misc.py b/rotkehlchen/utils/misc.py
--- a/rotkehlchen/utils/misc.py
+++ b/rotkehlchen/utils/misc.py
@@ -95,25 +95,34 @@ def cache_response_timewise() -> Callable:
     """
     def _cache_response_timewise(f: Callable):
         @wraps(f)
-        def wrapper(wrappingobj, *args):
+        def wrapper(wrappingobj, *args, **kwargs):
+            cache_key = f.__name__
+            for arg in args:
+                cache_key += str(arg)
+            for _, value in kwargs.items():
+                cache_key += str(value)
+            # TODO: Do I really need hash() here?
+            cache_key = hash(cache_key)
+
             with wrappingobj.lock:
                 now = ts_now()
-                if f.__name__ in wrappingobj.results_cache:
-                    cache_life_secs = now - wrappingobj.results_cache[f.__name__].timestamp
+                if cache_key in wrappingobj.results_cache:
+                    cache_life_secs = now - wrappingobj.results_cache[cache_key].timestamp
+
                 cache_miss = (
-                    f.__name__ not in wrappingobj.results_cache or
+                    cache_key not in wrappingobj.results_cache or
                     cache_life_secs >= wrappingobj.cache_ttl_secs
                 )
 
             if cache_miss:
-                result = f(wrappingobj, *args)
+                result = f(wrappingobj, *args, **kwargs)
                 with wrappingobj.lock:
-                    wrappingobj.results_cache[f.__name__] = ResultCache(result, now)
+                    wrappingobj.results_cache[cache_key] = ResultCache(result, now)
                 return result
 
             # else hit the cache
             with wrappingobj.lock:
-                return wrappingobj.results_cache[f.__name__].result
+                return wrappingobj.results_cache[cache_key].result
 
         return wrapper
     return _cache_response_timewise


Test patch:
diff --git a/rotkehlchen/tests/test_binance.py b/rotkehlchen/tests/test_binance.py
--- a/rotkehlchen/tests/test_binance.py
+++ b/rotkehlchen/tests/test_binance.py
@@ -271,7 +271,6 @@ def mock_unknown_asset_return(url):  # pylint: disable=unused-argument
 def test_binance_query_trade_history(function_scope_binance):
     """Test that turning a binance trade as returned by the server to our format works"""
     binance = function_scope_binance
-    binance.cache_ttl_secs = 0
 
     def mock_my_trades(url):  # pylint: disable=unused-argument
         if 'symbol=BNBBTC' in url:
@@ -459,7 +458,6 @@ def query_binance_and_test(
 def test_binance_query_deposits_withdrawals(function_scope_binance):
     """Test the happy case of binance deposit withdrawal query"""
     binance = function_scope_binance
-    binance.cache_ttl_secs = 0
 
     def mock_get_deposit_withdrawal(url):  # pylint: disable=unused-argument
         if 'deposit' in url:
@@ -515,7 +513,6 @@ def mock_get_deposit_withdrawal(url):  # pylint: disable=unused-argument
 def test_binance_query_deposits_withdrawals_unexpected_data(function_scope_binance):
     """Test that we handle unexpected deposit withdrawal query data gracefully"""
     binance = function_scope_binance
-    binance.cache_ttl_secs = 0
 
     def mock_binance_and_query(deposits, withdrawals, expected_warnings_num, expected_errors_num):
 
diff --git a/rotkehlchen/tests/test_bitmex.py b/rotkehlchen/tests/test_bitmex.py
--- a/rotkehlchen/tests/test_bitmex.py
+++ b/rotkehlchen/tests/test_bitmex.py
@@ -107,7 +107,6 @@ def test_bitmex_api_withdrawals_deposit_and_query_after_subquery(test_bitmex):
 
 def test_bitmex_api_withdrawals_deposit_unexpected_data(test_bitmex):
     """Test getting unexpected data in bitmex withdrawals deposit query is handled gracefully"""
-    test_bitmex.cache_ttl_secs = 0
 
     original_input = """[{
 "transactID": "b6c6fd2c-4d0c-b101-a41c-fa5aa1ce7ef1", "account": 126541, "currency": "XBt",
diff --git a/rotkehlchen/tests/test_bittrex.py b/rotkehlchen/tests/test_bittrex.py
--- a/rotkehlchen/tests/test_bittrex.py
+++ b/rotkehlchen/tests/test_bittrex.py
@@ -148,8 +148,6 @@ def mock_unknown_asset_return(url):  # pylint: disable=unused-argument
 
 def test_bittrex_query_trade_history(bittrex):
     """Test that turning a bittrex trade to our format works"""
-    # turn caching off
-    bittrex.cache_ttl_secs = 0
 
     def mock_order_history(url):  # pylint: disable=unused-argument
         response = MockResponse(200, BITTREX_ORDER_HISTORY_RESPONSE)
@@ -340,8 +338,6 @@ def query_bittrex_and_test(
 
 def test_bittrex_query_deposits_withdrawals(bittrex):
     """Test the happy case of bittrex deposit withdrawal query"""
-    # turn caching off
-    bittrex.cache_ttl_secs = 0
 
     def mock_get_deposit_withdrawal(url):  # pylint: disable=unused-argument
         if 'deposit' in url:
@@ -409,8 +405,6 @@ def mock_get_deposit_withdrawal(url):  # pylint: disable=unused-argument
 
 def test_bittrex_query_deposits_withdrawals_unexpected_data(bittrex):
     """Test that we handle unexpected bittrex deposit withdrawal data gracefully"""
-    # turn caching off
-    bittrex.cache_ttl_secs = 0
 
     def mock_bittrex_and_query(deposits, withdrawals, expected_warnings_num, expected_errors_num):
 
diff --git a/rotkehlchen/tests/test_coinbase.py b/rotkehlchen/tests/test_coinbase.py
--- a/rotkehlchen/tests/test_coinbase.py
+++ b/rotkehlchen/tests/test_coinbase.py
@@ -649,7 +649,6 @@ def test_coinbase_query_trade_history_unexpected_data(function_scope_coinbase):
 def test_coinbase_query_deposit_withdrawals(function_scope_coinbase):
     """Test that coinbase deposit/withdrawals history query works fine for the happy path"""
     coinbase = function_scope_coinbase
-    coinbase.cache_ttl_secs = 0
 
     with patch.object(coinbase.session, 'get', side_effect=mock_normal_coinbase_query):
         movements = coinbase.query_online_deposits_withdrawals(
@@ -713,7 +712,6 @@ def test_coinbase_query_deposit_withdrawals(function_scope_coinbase):
 def test_coinbase_query_deposit_withdrawals_unexpected_data(function_scope_coinbase):
     """Test that coinbase deposit/withdrawals query handles unexpected data properly"""
     coinbase = function_scope_coinbase
-    coinbase.cache_ttl_secs = 0
 
     # first query with proper data and expect no errors
     query_coinbase_and_test(
diff --git a/rotkehlchen/tests/test_kraken.py b/rotkehlchen/tests/test_kraken.py
--- a/rotkehlchen/tests/test_kraken.py
+++ b/rotkehlchen/tests/test_kraken.py
@@ -132,7 +132,6 @@ def test_kraken_query_deposit_withdrawals_unexpected_data(function_scope_kraken)
     """Test if a kraken deposits_withdrawals query returns invalid data we handle it gracefully"""
     kraken = function_scope_kraken
     kraken.random_ledgers_data = False
-    kraken.cache_ttl_secs = 0
 
     test_deposits = """{
     "ledger": {
diff --git a/rotkehlchen/tests/test_poloniex.py b/rotkehlchen/tests/test_poloniex.py
--- a/rotkehlchen/tests/test_poloniex.py
+++ b/rotkehlchen/tests/test_poloniex.py
@@ -213,7 +213,6 @@ def test_poloniex_trade_with_asset_needing_conversion():
 def test_query_trade_history(function_scope_poloniex):
     """Happy path test for poloniex trade history querying"""
     poloniex = function_scope_poloniex
-    poloniex.cache_ttl_secs = 0
 
     def mock_api_return(url, req):  # pylint: disable=unused-argument
         contents = """{ "BTC_BCH":
diff --git a/rotkehlchen/tests/test_utils.py b/rotkehlchen/tests/test_utils.py
--- a/rotkehlchen/tests/test_utils.py
+++ b/rotkehlchen/tests/test_utils.py
@@ -1,15 +1,22 @@
 import json
 import time
+from typing import Any, Dict
 from unittest.mock import patch
 
 import pytest
+from gevent.lock import Semaphore
 
 from rotkehlchen.errors import UnprocessableTradePair
 from rotkehlchen.exchanges.data_structures import invert_pair
 from rotkehlchen.fval import FVal
 from rotkehlchen.serialization.serialize import process_result
 from rotkehlchen.tests.utils.mock import MockResponse
-from rotkehlchen.utils.misc import combine_dicts, combine_stat_dicts, iso8601ts_to_timestamp
+from rotkehlchen.utils.misc import (
+    cache_response_timewise,
+    combine_dicts,
+    combine_stat_dicts,
+    iso8601ts_to_timestamp,
+)
 from rotkehlchen.utils.version_check import check_if_version_up_to_date
 
 
@@ -152,3 +159,41 @@ def mock_invalid_json_github_return(url):  # pylint: disable=unused-argument
     with patch('requests.get', side_effect=mock_invalid_json_github_return):
         result = check_if_version_up_to_date()
         assert not result
+
+
+class Foo():
+    def __init__(self):
+        self.lock = Semaphore()
+        self.results_cache: Dict[str, Any] = {}
+        self.cache_ttl_secs = 60
+
+        self.do_something_call_count = 0
+
+    @cache_response_timewise()
+    def do_sum(self, arg1, arg2):  # pylint: disable=no-self-use
+        return arg1 + arg2
+
+    @cache_response_timewise()
+    def do_something(self):
+        self.do_something_call_count += 1
+        return 5
+
+
+def test_cache_response_timewise():
+    """Test that cached value is called and not the function again"""
+    instance = Foo()
+
+    assert instance.do_something() == 5
+    assert instance.do_something() == 5
+
+    assert instance.do_something_call_count == 1
+
+
+def test_cache_response_timewise_different_args():
+    """Test that applying the cache timewise decorator works fine for different arguments
+
+    Regression test for https://github.com/rotki/rotki/issues/543
+    """
+    instance = Foo()
+    assert instance.do_sum(1, 1) == 2
+    assert instance.do_sum(2, 2) == 4