From 244826194b50476ad6ed3eeea898f34c4e0a06a5 Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Mon, 10 Jun 2013 00:13:29 +0200 Subject: [PATCH] [gen] Added a mechanism for caching method calls that are performed several times for displaying a single ui page (ie: field.show methods). --- gen/__init__.py | 93 ++++++++++++++++++------------------- gen/calendar.py | 2 +- gen/utils.py | 51 ++++++++++++++++++++ gen/wrappers/UserWrapper.py | 6 +-- 4 files changed, 100 insertions(+), 52 deletions(-) diff --git a/gen/__init__.py b/gen/__init__.py index e17e25f..2a162f5 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -7,14 +7,13 @@ from appy.gen.layout import Table from appy.gen.layout import defaultFieldLayouts from appy.gen.mail import sendNotification from appy.gen.indexer import defaultIndexes, XhtmlTextExtractor -from appy.gen.utils import GroupDescr, Keywords, getClassName, SomeObjects +from appy.gen import utils as gutils import appy.pod from appy.pod.renderer import Renderer from appy.shared.data import countries from appy.shared.xml_parser import XhtmlCleaner from appy.shared.diff import HtmlDiff -from appy.shared.utils import Traceback, getOsTempFolder, formatNumber, \ - FileWrapper, sequenceTypes +from appy.shared import utils as sutils # Default Appy permissions ----------------------------------------------------- r, w, d = ('read', 'write', 'delete') @@ -30,7 +29,7 @@ labelTypes = ('label', 'descr', 'help') def initMasterValue(v): '''Standardizes p_v as a list of strings.''' if not isinstance(v, bool) and not v: res = [] - elif type(v) not in sequenceTypes: res = [v] + elif type(v) not in sutils.sequenceTypes: res = [v] else: res = v return [str(v) for v in res] @@ -249,8 +248,8 @@ class Group: # First, create the corresponding GroupDescr if not already in # p_groupDescrs. if self.name not in groupDescrs: - groupDescr = groupDescrs[self.name] = \ - GroupDescr(self, page, metaType, forSearch=forSearch).get() + groupDescr = groupDescrs[self.name] = gutils.GroupDescr(\ + self, page, metaType, forSearch=forSearch).get() # Insert the group at the higher level (ie, directly in p_widgets) # if the group is not itself in a group. if not self.group: @@ -258,7 +257,7 @@ class Group: else: outerGroupDescr = self.group.insertInto(widgets, groupDescrs, page, metaType, forSearch=forSearch) - GroupDescr.addWidget(outerGroupDescr, groupDescr) + gutils.GroupDescr.addWidget(outerGroupDescr, groupDescr) else: groupDescr = groupDescrs[self.name] return groupDescr @@ -348,12 +347,12 @@ class Search: if (field and (field.getIndexType() == 'TextIndex')) or \ (fieldName == 'SearchableText'): # For TextIndex indexes. We must split p_fieldValue into keywords. - res = Keywords(fieldValue).get() + res = gutils.Keywords(fieldValue).get() elif isinstance(fieldValue, basestring) and fieldValue.endswith('*'): v = fieldValue[:-1] # Warning: 'z' is higher than 'Z'! res = {'query':(v,v+'z'), 'range':'min:max'} - elif type(fieldValue) in sequenceTypes: + elif type(fieldValue) in sutils.sequenceTypes: if fieldValue and isinstance(fieldValue[0], basestring): # We have a list of string values (ie: we need to # search v1 or v2 or...) @@ -410,7 +409,7 @@ class Search: def isShowable(self, klass, tool): '''Is this Search instance (defined in p_klass) showable?''' if self.show.__class__.__name__ == 'staticmethod': - return self.show.__get__(klass)(tool) + return gutils.callMethod(tool, self.show, klass=klass) return self.show # ------------------------------------------------------------------------------ @@ -540,7 +539,7 @@ class Type: self.name = name # Determine prefix for this class if not klass: prefix = appName - else: prefix = getClassName(klass, appName) + else: prefix = gutils.getClassName(klass, appName) # Recompute the ID (and derived attributes) that may have changed if # we are in debug mode (because we recreate new Type instances). self.id = id(self) @@ -628,7 +627,7 @@ class Type: else: res = self.show # Take into account possible values 'view', 'edit', 'result'... - if type(res) in sequenceTypes: + if type(res) in sutils.sequenceTypes: for r in res: if r == layoutType: return True return False @@ -645,7 +644,7 @@ class Type: master, masterValue = masterData reqValue = master.getRequestValue(obj.REQUEST) # reqValue can be a list or not - if type(reqValue) not in sequenceTypes: + if type(reqValue) not in sutils.sequenceTypes: return reqValue in masterValue else: for m in masterValue: @@ -847,7 +846,7 @@ class Type: if forSearch and (value != None): if isinstance(value, unicode): res = value.encode('utf-8') - elif type(value) in sequenceTypes: + elif type(value) in sutils.sequenceTypes: res = [] for v in value: if isinstance(v, unicode): res.append(v.encode('utf-8')) @@ -966,7 +965,7 @@ class Type: p_self type definition on p_obj.''' setattr(obj, self.name, value) - def callMethod(self, obj, method, raiseOnError=True): + def callMethod(self, obj, method, cache=True): '''This method is used to call a p_method on p_obj. p_method is part of this type definition (ie a default method, the method of a Computed field, a method used for showing or not a field...). Normally, those @@ -975,26 +974,22 @@ class Type: p_method with no arg *or* with the field arg.''' obj = obj.appy() try: - return method(obj) + return gutils.callMethod(obj, method, cache=cache) except TypeError, te: # Try a version of the method that would accept self as an - # additional parameter. - tb = Traceback.get() + # additional parameter. In this case, we do not try to cache the + # value (we do not call gutils.callMethod), because the value may + # be different depending on the parameter. + tb = sutils.Traceback.get() try: return method(obj, self) except Exception, e: obj.log(tb, type='error') - if raiseOnError: - # Raise the initial error. - raise te - else: - return str(te) + # Raise the initial error. + raise te except Exception, e: - obj.log(Traceback.get(), type='error') - if raiseOnError: - raise e - else: - return str(e) + obj.log(sutils.Traceback.get(), type='error') + raise e def process(self, obj): '''This method is a general hook allowing a field to perform some @@ -1047,7 +1042,7 @@ class Float(Type): self.precision = precision # The decimal separator can be a tuple if several are allowed, ie # ('.', ',') - if type(sep) not in sequenceTypes: + if type(sep) not in sutils.sequenceTypes: self.sep = (sep,) else: self.sep = sep @@ -1065,8 +1060,8 @@ class Float(Type): self.pythonType = float def getFormattedValue(self, obj, value, showChanges=False): - return formatNumber(value, sep=self.sep[0], precision=self.precision, - tsep=self.tsep) + return sutils.formatNumber(value, sep=self.sep[0], + precision=self.precision, tsep=self.tsep) def validateValue(self, obj, value): # Replace used separator with the Python separator '.' @@ -1487,7 +1482,7 @@ class String(Type): value = value[:self.maxChars] # Get a multivalued value if required. if value and self.isMultiValued() and \ - (type(value) not in sequenceTypes): + (type(value) not in sutils.sequenceTypes): value = [value] return value @@ -1701,12 +1696,12 @@ class File(Type): res.filename = fileName res.content_type = mimetypes.guess_type(fileName)[0] f.close() - if not zope: res = FileWrapper(res) + if not zope: res = sutils.FileWrapper(res) return res def getValue(self, obj): value = Type.getValue(self, obj) - if value: value = FileWrapper(value) + if value: value = sutils.FileWrapper(value) return value def getFormattedValue(self, obj, value, showChanges=False): @@ -1786,11 +1781,11 @@ class File(Type): setattr(obj, self.name, existingValue) elif isinstance(value, OFSImageFile): setattr(obj, self.name, value) - elif isinstance(value, FileWrapper): + elif isinstance(value, sutils.FileWrapper): setattr(obj, self.name, value._zopeFile) elif isinstance(value, basestring): setattr(obj, self.name, File.getFileObject(value, zope=True)) - elif type(value) in sequenceTypes: + elif type(value) in sutils.sequenceTypes: # It should be a 2-tuple or 3-tuple fileName = None mimeType = None @@ -1956,13 +1951,13 @@ class Ref(Type): if defValue: # I must prefix call to function "type" with "__builtins__" # because this name was overridden by a method parameter. - if __builtins__['type'](defValue) in sequenceTypes: + if __builtins__['type'](defValue) in sutils.sequenceTypes: uids = [o.o.UID() for o in defValue] else: uids = [defValue.o.UID()] # Prepare the result: an instance of SomeObjects, that will be unwrapped # if not required. - res = SomeObjects() + res = gutils.SomeObjects() res.totalNumber = res.batchSize = len(uids) batchNeeded = startNumber != None if batchNeeded: @@ -2040,7 +2035,7 @@ class Ref(Type): '''This method links p_value (which can be a list of objects) to p_obj through this Ref field.''' # p_value can be a list of objects - if type(value) in sequenceTypes: + if type(value) in sutils.sequenceTypes: for v in value: self.linkObject(obj, v, back=back) return # Gets the list of referred objects (=list of uids), or create it. @@ -2068,7 +2063,7 @@ class Ref(Type): '''This method unlinks p_value (which can be a list of objects) from p_obj through this Ref field.''' # p_value can be a list of objects - if type(value) in sequenceTypes: + if type(value) in sutils.sequenceTypes: for v in value: self.unlinkObject(obj, v, back=back) return obj = obj.o @@ -2093,7 +2088,7 @@ class Ref(Type): # Standardize p_value into a list of Zope objects objects = value if not objects: objects = [] - if type(objects) not in sequenceTypes: objects = [objects] + if type(objects) not in sutils.sequenceTypes: objects = [objects] tool = obj.getTool() for i in range(len(objects)): if isinstance(objects[i], basestring): @@ -2234,7 +2229,7 @@ class Computed(Type): return self.callMacro(obj, self.method) else: # self.method is a method that will return the field value - return self.callMethod(obj, self.method, raiseOnError=True) + return self.callMethod(obj, self.method, cache=False) def getFormattedValue(self, obj, value, showChanges=False): if not isinstance(value, basestring): return str(value) @@ -2278,12 +2273,12 @@ class Action(Type): def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'} def __call__(self, obj): '''Calls the action on p_obj.''' - if type(self.action) in sequenceTypes: + if type(self.action) in sutils.sequenceTypes: # There are multiple Python methods res = [True, ''] for act in self.action: actRes = act(obj) - if type(actRes) in sequenceTypes: + if type(actRes) in sutils.sequenceTypes: res[0] = res[0] and actRes[0] if self.result.startswith('file'): res[1] = res[1] + actRes[1] @@ -2294,7 +2289,7 @@ class Action(Type): else: # There is only one Python method actRes = self.action(obj) - if type(actRes) in sequenceTypes: + if type(actRes) in sutils.sequenceTypes: res = list(actRes) else: res = [actRes, ''] @@ -2395,7 +2390,9 @@ class Pod(Type): retrieved by calling pod to compute the result.''' rq = getattr(obj, 'REQUEST', None) res = getattr(obj.aq_base, self.name, None) - if res and res.size: return FileWrapper(res) # Return the frozen file. + if res and res.size: + # Return the frozen file. + return sutils.FileWrapper(res) # If we are here, it means that we must call pod to compute the file. # A Pod field differs from other field types because there can be # several ways to produce the field value (ie: output file format can be @@ -2419,7 +2416,7 @@ class Pod(Type): specificContext = self.context # Temporary file where to generate the result tempFileName = '%s/%s_%f.%s' % ( - getOsTempFolder(), obj.uid, time.time(), outputFormat) + sutils.getOsTempFolder(), obj.uid, time.time(), outputFormat) # Define parameters to give to the appy.pod renderer podContext = {'tool': tool, 'user': obj.user, 'self': obj, 'field':self, 'now': obj.o.getProductConfig().DateTime(), @@ -2486,7 +2483,7 @@ class Pod(Type): def store(self, obj, value): '''Stores (=freezes) a document (in p_value) in the field.''' - if isinstance(value, FileWrapper): + if isinstance(value, sutils.FileWrapper): value = value._zopeFile setattr(obj, self.name, value) diff --git a/gen/calendar.py b/gen/calendar.py index ae41c7e..7272a71 100644 --- a/gen/calendar.py +++ b/gen/calendar.py @@ -164,7 +164,7 @@ class Calendar(Type): '''Returns the list of other calendars whose events must also be shown on this calendar.''' if self.otherCalendars: - res = self.callMethod(obj, self.otherCalendars, preComputed) + res = self.otherCalendars(obj.appy(), preComputed) # Replace field names with field objects for i in range(len(res)): res[i][1] = res[i][0].getField(res[i][1]) diff --git a/gen/utils.py b/gen/utils.py index ba0506c..2a2aae2 100644 --- a/gen/utils.py +++ b/gen/utils.py @@ -306,4 +306,55 @@ def updateRolesForPermission(permission, roles, obj): existingRoles = perm.getRoles() allRoles = set(existingRoles).union(roles) obj.manage_permission(permission, tuple(allRoles), acquire=0) + +# ------------------------------------------------------------------------------ +def callMethod(obj, method, klass=None, cache=True): + '''This function is used to call a p_method on some Appy p_obj. m_method + can be an instance method on p_obj; it can also be a static method. In + this latter case, p_obj is the tool and the static method, defined in + p_klass, will be called with the tool as unique arg. + + A method cache is implemented on the request object (available at + p_obj.request). So while handling a single request from the ui, every + method is called only once. Some method calls must not be cached (ie, + values of Computed fields). In this case, p_cache will be False.''' + rq = obj.request + # Create the method cache if it does not exist on the request + if not hasattr(rq, 'methodCache'): rq.methodCache = {} + # If m_method is a static method or an instance method, unwrap the true + # Python function object behind it. + methodType = method.__class__.__name__ + if methodType == 'staticmethod': + method = method.__get__(klass) + elif methodType == 'instancemethod': + method = method.im_func + # Call the method if cache is not needed. + if not cache: return method(obj) + # If first arg of method is named "tool" instead of the traditional "self", + # we cheat and will call the method with the tool as first arg. This will + # allow to consider this method as if it was a static method on the tool. + # Every method call, even on different instances, will be cached in a unique + # key. + cheat = False + if not klass and (method.func_code.co_varnames[0] == 'tool'): + prefix = obj.klass.__name__ + obj = obj.tool + cheat = True + # Build the key of this method call in the cache. + # First part of the key: the p_obj's uid (if p_method is an instance method) + # or p_className (if p_method is a static method). + if not cheat: + if klass: + prefix = klass.__name__ + else: + prefix = obj.uid + # Second part of the key: p_method name + key = '%s:%s' % (prefix, method.func_name) + # Return the cached value if present in the method cache. + if key in rq.methodCache: + return rq.methodCache[key] + # No cached value: call the method, cache the result and return it + res = method(obj) + rq.methodCache[key] = res + return res # ------------------------------------------------------------------------------ diff --git a/gen/wrappers/UserWrapper.py b/gen/wrappers/UserWrapper.py index cbaba23..7f01172 100644 --- a/gen/wrappers/UserWrapper.py +++ b/gen/wrappers/UserWrapper.py @@ -13,7 +13,7 @@ class UserWrapper(AbstractWrapper): if self.user.has_role('Manager'): return True return ('view', 'result') - def showName(self): + def showName(tool): '''Name and first name, by default, are always shown.''' return True @@ -23,9 +23,9 @@ class UserWrapper(AbstractWrapper): email = self.email return email and (email != self.login) - def showRoles(self): + def showRoles(tool): '''Only the admin can view or edit roles.''' - return self.user.has_role('Manager') + return tool.user.has_role('Manager') def validateLogin(self, login): '''Is this p_login valid?'''