6

flask_cache.Cache.memoize not working with flask_restful.Resource

Here is sample code:

from flask import Flask, request, jsonify
from flask_restful import Resource, Api
from flask_cache import Cache

app = Flask(__name__)
api = Api(app)
cache = Cache(app, config={'CACHE_TYPE': 'simple'})


class MyResource(Resource):
    JSONIFY = True
    PATH = None
    ENDPOINT = None

    def dispatch_request(self, *args, **kwargs):
        kw = dict(**kwargs)
        kw.update(request.args.items())
        r = super().dispatch_request(*args, **kw)
        if self.JSONIFY:
            return jsonify(r)
        else:
            return r


class DebugResource(MyResource):
    PATH = '/debug'
    ENDPOINT = 'debug'

    @cache.memoize(timeout=30)
    def get(self, **kwargs):
        print('cache is not used!')
        return kwargs

for r in [DebugResource]:
    api.add_resource(r, r.PATH, endpoint=r.ENDPOINT)


print('running!')
app.run()

Notice that in get() I added print so I can see when the code is actually called and when cached value is used.

I start server then in browser i go to http://localhost:5000/debug?a=1 and press f5 repeatetely. I expect that my function get is called once and then cached value is used. But in server console I see my print each time I press f5. So memoize is not working. What am I doing wrong?

edit:

I moved my cached function outside from Resource class

@cache.memoize(timeout=30)
def my_foo(a):
    print('cache is not used!')
    return dict(kw=a, id=id(a))

class DebugResource(MyResource):
    PATH = '/debug'
    ENDPOINT = 'debug'

    def get(self, a):
        return my_foo(a)

and that worked. As far as I can see, the issue was self argument that was actually unique in each call. The question is still, how to make it work without extracting additional function for each method i want to cache? Current solution looks like a workaround.

Cœur
  • 37,241
  • 25
  • 195
  • 267
Rugnar
  • 2,894
  • 3
  • 25
  • 29
  • I don't know who clicked `downvote` for your question, but I don't understand why you clicked `downvote` for my answer. I wanted to help and I'm sure my solution is working. Could you explain why you did it? – Danila Ganchar Mar 14 '17 at 16:52
  • Sorry for that, I clicked `downvote` because wasn't what I expected, though you solution will work. I'd like to change my vote back but I'm not alloved to until you edit you answer.My point was that I want my cache to take function args into account and only them. So I can decorate resource methods and any other function with the same way. So it is not suitable for me when cache uses `request.path` as key instead on args. And the problem was that arg `self`affects cache key calculation and makes it unique every request. – Rugnar Mar 15 '17 at 11:37
  • Can you make rollback `downvote`? I updated my answer. – Danila Ganchar Mar 15 '17 at 12:39
  • thanks. good luck in development ;) – Danila Ganchar Mar 15 '17 at 13:35

4 Answers4

6

The cache doesn't work because you use memoize method. In this case it will cache the result of a function. Decorator doesn't know anything about route(view, path).

To fix it you should use cached method. @cached decorator has argument key_prefix with default value = view/request.path.

So, just change @cache.memoize(timeout=30) to @cache.cached(timeout=30)

Danila Ganchar
  • 10,266
  • 13
  • 49
  • 75
3

Thank @Rugnar, this decision came in handy.

solution

The only point, I had to change it a bit, so that I would not exclude the first element (self), but use it, in order to store more unique keys in a situation where the cached method is defined in the base class, and in the children they are customized.

Method _extract_self_arg updated.

class ResourceCache(Cache):
""" When the class method is being memoized,
    cache key uses the class name from self or cls."""

def _memoize_make_cache_key(self, make_name=None, timeout=None):
    def make_cache_key(f, *args, **kwargs):
        fname, _ = function_namespace(f)
        if callable(make_name):
            altfname = make_name(fname)
        else:
            altfname = fname
        updated = altfname + json.dumps(dict(
            args=self._extract_self_arg(f, args),
            kwargs=kwargs), sort_keys=True)
        return b64encode(
            md5(updated.encode('utf-8')).digest()
        )[:16].decode('utf-8')

    return make_cache_key

@staticmethod
def _extract_self_arg(f, args):
    argspec_args = inspect.getargspec(f).args

    if argspec_args and argspec_args[0] in ('self', 'cls'):
        if hasattr(args[0], '__name__'):
            return (args[0].__name__,) + args[1:]
        return (args[0].__class__.__name__,) + args[1:]
    return args

Maybe it will also be useful to someone.

1

It doesn't work because memoize takes function's arguments into account in the cache key and every new request gets unique kwargs (unique result of id function).

To see, simply modify the code

@cache.memoize(timeout=30)
def get(self, **kwargs):
    print('cache is not used!')
    return id(kwargs)

and every new request you will get another result. So every new request cache key is different, that's why you see cache is not used! on the console output.

Piotr Dawidiuk
  • 2,961
  • 1
  • 24
  • 33
  • using `**kwargs` is not good solution, because other developers will not see signature of function. input data will be like a black box. – Danila Ganchar Mar 11 '17 at 18:38
  • @DanilaGanchar I am not saying it's a good practice, that's only an OP's code and I used this to describe why cache "doesn't work". – Piotr Dawidiuk Mar 11 '17 at 18:42
  • I tried this, `id` is not changed, but the problem is still `@cache.memoize(timeout=30)` `def get(self, a):` `print('cache is not used!')` `return dict(kw=a, id=id(a))` – Rugnar Mar 13 '17 at 09:38
  • @Rugnar Just use `@cache.cached` as Danila Ganchar has suggested and define callable `key_prefix`: http://stackoverflow.com/a/14264116/3866610 – Piotr Dawidiuk Mar 13 '17 at 20:43
0

Found solution by subclassing Cache and overloading logic that creates cache key for memoize. So it works fine.

import json
import inspect
from base64 import b64encode
from hashlib import md5
from flask_cache import Cache, function_namespace

class ResourceCache(Cache):
    def _memoize_make_cache_key(self, make_name=None, timeout=None):
        def make_cache_key(f, *args, **kwargs):
            fname, _ = function_namespace(f)
            if callable(make_name):
                altfname = make_name(fname)
            else:
                altfname = fname

            updated = altfname + json.dumps(dict(
                args=self._extract_self_arg(f, args),
                kwargs=kwargs), sort_keys=True)

            return b64encode(
                md5(updated.encode('utf-8')).digest()
            )[:16].decode('utf-8')

        return make_cache_key

    @staticmethod
    def _extract_self_arg(f, args):
        argspec_args = inspect.getargspec(f).args
        if argspec_args and argspec_args[0] in ('self', 'cls'):
            return args[1:]
        return args

In other words, when class method is being memoized, cache ignores the first argument self or cls.

Rugnar
  • 2,894
  • 3
  • 25
  • 29