From 4e9cea7e60180cafc0bc3e1c61b13cf05cd2603d Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Thu, 6 Jul 2023 16:58:50 +0200 Subject: [PATCH] Fix plone.memoize.view to support unhashable types in function arguments. Fixes: #36 --- news/36.bugfix | 3 +++ plone/memoize/view.py | 10 +++++--- plone/memoize/view.rst | 57 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 news/36.bugfix diff --git a/news/36.bugfix b/news/36.bugfix new file mode 100644 index 0000000..c8ab335 --- /dev/null +++ b/news/36.bugfix @@ -0,0 +1,3 @@ +Fix plone.memoize.view to support unhashable types in function arguments. + +Fixes: #36 diff --git a/plone/memoize/view.py b/plone/memoize/view.py index a21e385..25cf2b1 100644 --- a/plone/memoize/view.py +++ b/plone/memoize/view.py @@ -6,6 +6,8 @@ from zope.annotation.interfaces import IAnnotations from zope.globalrequest import getRequest +import json + class ViewMemo: key = "plone.memoize" @@ -43,8 +45,8 @@ def memogetter(*args, **kwargs): context_id, instance.__class__.__name__, func.__name__, - args[1:], - frozenset(kwargs.items()), + json.dumps(args[1:]), + json.dumps(kwargs), ) if key not in cache: cache[key] = func(*args, **kwargs) @@ -72,8 +74,8 @@ def memogetter(*args, **kwargs): key = ( instance.__class__.__name__, func.__name__, - args[1:], - frozenset(kwargs.items()), + json.dumps(args[1:]), + json.dumps(kwargs), ) if key not in cache: cache[key] = func(*args, **kwargs) diff --git a/plone/memoize/view.rst b/plone/memoize/view.rst index 45de49c..e78042a 100644 --- a/plone/memoize/view.rst +++ b/plone/memoize/view.rst @@ -33,14 +33,16 @@ First we set up a dummy view:: ... return '%s world' % self.txt1 ... ... @view.memoize - ... def getMsg(self, to, **instruction): - ... lst = ['%s--%s' %t for t in sorted(instruction.items(), reverse=True)] + ... def getMsg(self, to, *args, **kwargs): + ... lst = ['%s' %t for t in args] + ... lst = lst + ['%s--%s' %t for t in sorted(kwargs.items(), reverse=True)] ... instxt = ' '.join(lst) ... return ("%s: %s world%s %s" %(to, self.txt1, self.bang, instxt)).strip() ... ... @view.memoize_contextless - ... def getAnotherMsg(self, to, **instruction): - ... lst = ['%s--%s' %t for t in instruction.items()] + ... def getAnotherMsg(self, to, *args, **kwargs): + ... lst = ['%s' %t for t in args] + ... lst = lst + ['%s--%s' %t for t in kwargs.items()] ... instxt = ' '.join(lst) ... return ("%s: %s world%s %s" %(to, self.txt1, self.bang, instxt)).strip() ... @@ -81,7 +83,8 @@ Even though we've twiddled txt1, txt2 is not recalculated:: >>> msg.txt2 'hello world' -We support memoization of multiple signatures as long as all signature values are hashable:: +We support memoization of multiple signatures. +The signature values just need to be able to be converted into a JSON string:: >>> print(msg.getMsg('Ernest')) Ernest: goodbye cruel world! @@ -89,6 +92,22 @@ We support memoization of multiple signatures as long as all signature values ar >>> print(msg.getMsg('J.D.', **{'raise':'roofbeams'})) J.D.: goodbye cruel world! raise--roofbeams +We also support unhashable types like lists and dicts:: + + >>> print(msg.getMsg('J.D.', lower=['shields', 'engines'])) + J.D.: goodbye cruel world! lower--['shields', 'engines'] + + >>> print(msg.getMsg('J.D.', actions={'open': ['gates']})) + J.D.: goodbye cruel world! actions--{'open': ['gates']} + +Also in non-keyword arguments:: + + >>> print(msg.getMsg('J.D.', ['shields', 'engines'])) + J.D.: goodbye cruel world! ['shields', 'engines'] + + >>> print(msg.getMsg('J.D.', {'open': ['gates']})) + J.D.: goodbye cruel world! {'open': ['gates']} + We can alter data underneath, but nothing changes:: >>> msg.txt1 = 'sound and fury' @@ -173,6 +192,34 @@ based on parameters, but not on context:: >>> print(msg2.getAnotherMsg('J.D.', **{'raise':'roofbeams'})) J.D.: so long, cruel world& raise--roofbeams +Contextless memoizing also supports unhashable types like lists and dicts:: + + >>> print(msg3.getAnotherMsg('J.D.', lower=['shields', 'engines'])) + J.D.: so long, cruel world& lower--['shields', 'engines'] + + >>> print(msg2.getAnotherMsg('J.D.', lower=['shields', 'engines'])) + J.D.: so long, cruel world& lower--['shields', 'engines'] + + >>> print(msg3.getAnotherMsg('J.D.', actions={'open': ['gates']})) + J.D.: so long, cruel world& actions--{'open': ['gates']} + + >>> print(msg2.getAnotherMsg('J.D.', actions={'open': ['gates']})) + J.D.: so long, cruel world& actions--{'open': ['gates']} + +Also in non-keyword arguments:: + + >>> print(msg3.getAnotherMsg('J.D.', ['shields', 'engines'])) + J.D.: so long, cruel world& ['shields', 'engines'] + + >>> print(msg2.getAnotherMsg('J.D.', ['shields', 'engines'])) + J.D.: so long, cruel world& ['shields', 'engines'] + + >>> print(msg3.getAnotherMsg('J.D.', {'open': ['gates']})) + J.D.: so long, cruel world& {'open': ['gates']} + + >>> print(msg2.getAnotherMsg('J.D.', {'open': ['gates']})) + J.D.: so long, cruel world& {'open': ['gates']} + There is also support for using a global request if zope.globalrequest is available. With that you can cache also functions.