source: indico/indico/MaKaC/common/fossilize.py @ 6245d8

burotelhello-world-walkthroughipv6new-webexv0.97-seriesv0.98-seriesv0.98.2v0.98.3v0.98b1v0.98b2v0.99v1.0v1.1
Last change on this file since 6245d8 was 6245d8, checked in by Pedro Ferreira <jose.pedro.ferreira@…>, 3 years ago

[FIX] Fossilization of PersistentList? was failing

  • Changed the criteria from a set of classes to the existence of an iter method;
  • Property mode set to 100644
File size: 12.4 KB
Line 
1# -*- coding: utf-8 -*-
2##
3##
4## This file is part of CDS Indico.
5## Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 CERN.
6##
7## CDS Indico is free software; you can redistribute it and/or
8## modify it under the terms of the GNU General Public License as
9## published by the Free Software Foundation; either version 2 of the
10## License, or (at your option) any later version.
11##
12## CDS Indico is distributed in the hope that it will be useful, but
13## WITHOUT ANY WARRANTY; without even the implied warranty of
14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15## General Public License for more details.
16##
17## You should have received a copy of the GNU General Public License
18## along with CDS Indico; if not, write to the Free Software Foundation, Inc.,
19## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
20
21"""
22`fossilize` allows us to "serialize" complex python objects into dictionaries
23and lists. Such operation is very useful for generating JSON data structures
24from business objects. It works as a wrapper around `zope.interface`.
25
26Some of the features are:
27 * Different "fossil" types for the same source class;
28 * Built-in inheritance support;
29"""
30
31import inspect
32import re
33import zope.interface
34from types import NoneType, ClassType, TypeType
35
36
37def fossilizes(*classList):
38    """
39    Simple wrapper around 'implements'
40    """
41    zope.interface.declarations._implements("fossilizes",
42                                            classList,
43                                            zope.interface.classImplements)
44
45def addFossil(klazz, fossils):
46    """
47    Declares fossils for a class
48
49    :param klazz: a class object
50    :type klass: class object
51    :param fossils: a fossil class (or a list of fossil classes)
52    """
53    if not type(fossils) is list:
54        fossils = [fossils]
55
56    for fossil in fossils:
57        zope.interface.classImplements(klazz, fossil)
58
59class NonFossilizableException(Exception):
60    """
61    Object is not fossilizable (doesn't implement Fossilizable)
62    """
63
64class WrongFossilTypeException(Exception):
65    """
66    Fossil type doesn't apply to target object
67    """
68
69class InvalidFossilException(Exception):
70    """
71    The fossil name doesn't follow the convention I(\w+)Fossil
72    or has an invalid method name and did not declare a .name tag for it
73    """
74
75class IFossil(zope.interface.Interface):
76    """
77    Fossil base interface. All fossil classes should derive from this one.
78    """
79
80class Fossilizable:
81    """
82    Base class for all the objects that can be fossilized
83    """
84
85    __fossilNameRE = re.compile('^I(\w+)Fossil$')
86    __methodNameRE = re.compile('^get(\w+)|(has\w+)|(is\w+)$')
87    __methodNameCache = {}
88    __fossilNameCache = {}
89    __fossilAttrsCache = {} # Attribute Cache for Fossils with
90                            # fields that are repeated
91
92    @classmethod
93    def __extractName(cls, name):
94        """
95        'De-camelcase' the name
96        """
97
98        if name in cls.__methodNameCache:
99            return cls.__methodNameCache[name]
100        else:
101            nmatch = cls.__methodNameRE.match(name)
102
103            if not nmatch:
104                raise InvalidFossilException("method name '%s' is not valid! "
105                                             "has to start by 'get', 'has', 'is' "
106                                             "or use 'name' tag" % name)
107            else:
108                group = nmatch.group(1) or nmatch.group(2) or nmatch.group(3)
109                extractedName = group[0:1].lower() + group[1:]
110                cls.__methodNameCache[name] = extractedName
111                return extractedName
112
113    @classmethod
114    def __extractFossilName(cls, name):
115        """
116        Extracts the fossil name from a I(.*)Fossil
117        class name.
118        IMyObjectBasicFossil -> myObjectBasic
119        """
120
121        if name in cls.__fossilNameCache:
122            fossilName = cls.__fossilNameCache[name]
123        else:
124            fossilNameMatch = Fossilizable.__fossilNameRE.match(name)
125            if fossilNameMatch is None:
126                raise InvalidFossilException("Invalid fossil name: %s."
127                                             " A fossil name should follow the"
128                                             " pattern: I(\w+)Fossil." % name)
129            else:
130                fossilName = fossilNameMatch.group(1)[0].lower() + fossilNameMatch.group(1)[1:]
131                cls.__fossilNameCache[name] = fossilName
132        return fossilName
133
134
135    def __obtainInterface(self, interfaceArg):
136        """
137        Obtains the appropriate interface for this object.
138
139        :param interfaceArg: the target fossile type
140        :type interfaceArg: IFossil, NoneType, or dict
141
142            -If IFossil, we will use it.
143            -If None, we will take the default fossil (the first one of this class's "fossilizes" list)
144            -If a dict, we will use the object's class, class name, or full class name as key.
145
146        Also verifies that the interface obtained through these 3 methods is effectively provided by the object.
147        """
148        if interfaceArg is None:
149            #we try to take the 1st interface declared with fossilizes
150            implementedInterfaces = list(i for i in zope.interface.implementedBy(self.__class__) if i.extends(IFossil))
151            if not implementedInterfaces:
152                raise NonFossilizableException("Object %s of class %s cannot be fossilized,"
153                                               "no fossils were declared for it" %
154                                               (str(self),
155                                                self.__class__.__name__))
156            else:
157                interface = implementedInterfaces[0]
158
159        elif type(interfaceArg) is dict:
160
161            className = self.__class__.__module__ + '.' + \
162                        self.__class__.__name__
163
164            # interfaceArg is a dictionary of class:Fossil pairs
165            if className in interfaceArg:
166                interface = interfaceArg[className]
167            else:
168                raise NonFossilizableException("Object %s of class %s cannot be fossilized; "
169                                               "its class was not a key in the provided fossils dictionary" %
170                                               (str(self),
171                                                self.__class__.__name__))
172        else:
173            interface = interfaceArg
174
175        if not interface.providedBy(self):
176
177            raise WrongFossilTypeException("Interface '%s' not provided"
178                                           " by '%s'" %
179                                           (interface.__name__,
180                                            self.__class__.__name__))
181
182        return interface
183
184
185    @classmethod
186    def _fossilizeIterable(cls, target, interface, useAttrCache = False, **kwargs):
187        """
188        Fossilizes an object, be it a 'direct' fossilizable
189        object, or an iterable (dict, list, set);
190        """
191
192        if isinstance(target, Fossilizable):
193            return target.fossilize(interface, useAttrCache, **kwargs)
194        else:
195            ttype = type(target)
196            if ttype in [int, str, float, NoneType]:
197                return target
198            elif ttype is dict:
199                container = {}
200                for key, value in target.iteritems():
201                    container[key] = fossilize(value, interface, useAttrCache, **kwargs)
202                return container
203            elif hasattr(target, '__iter__'):
204                #we turn sets and tuples into lists since JSON does not have sets / tuples
205                return list(fossilize(elem,
206                                      interface,
207                                      useAttrCache,
208                                      **kwargs) for elem in target)
209            else:
210                raise NonFossilizableException("Type %s is not fossilizable!" %
211                                               ttype)
212
213            return fossilize(target, interface, useAttrCache)
214
215    def fossilize(self, interfaceArg = None, useAttrCache = False, **kwargs):
216        """
217        Fossilizes the object, using the fossil provided by `interface`.
218
219        :param interfaceArg: the target fossile type
220        :type interfaceArg: IFossil, NoneType, or dict
221        :param useAttrCache: use caching of attributes if same fields are
222            repeated for a fossil
223        :type useAttrCache: boolean
224        """
225
226        interface = self.__obtainInterface(interfaceArg)
227
228        name = interface.getName()
229        fossilName = self.__extractFossilName(name)
230
231        result = {}
232
233        for method in interface:
234
235            tags = interface[method].getTaggedValueTags()
236
237            # In some cases it is better to use the attribute cache to
238            # speed up the fossilization
239            cacheUsed = False
240            if useAttrCache:
241                try:
242                    methodResult = self.__fossilAttrsCache[self._p_oid][method]
243                    cacheUsed = True
244                except KeyError:
245                    pass
246            if not cacheUsed:
247                #Please use 'produce' as little as possible; there is almost always a more elegant and modular solution!
248                if 'produce' in tags:
249                    methodResult = interface[method].getTaggedValue('produce')(self)
250                else:
251                    methodResult = getattr(self, method)()
252
253                if hasattr(self, "_p_oid"):
254                    try:
255                        self.__fossilAttrsCache[self._p_oid]
256                    except KeyError:
257                        self.__fossilAttrsCache[self._p_oid] = {}
258                    self.__fossilAttrsCache[self._p_oid][method] = methodResult
259
260            # Result conversion
261            if 'result' in tags:
262                targetInterface = interface[method].getTaggedValue('result')
263                #targetInterface = globals()[targetInterfaceName]
264
265                methodResult = Fossilizable._fossilizeIterable(
266                    methodResult, targetInterface, **kwargs)
267
268            # Conversion function
269            if 'convert' in tags:
270                convertFunction = interface[method].getTaggedValue('convert')
271                converterArgNames = inspect.getargspec(convertFunction)[0]
272                converterArgs = dict((name, kwargs[name])
273                                     for name in converterArgNames
274                                     if name in kwargs)
275                methodResult = convertFunction(methodResult, **converterArgs)
276
277            # Re-name the attribute produced by the method
278            if 'name' in tags:
279                attrName = interface[method].getTaggedValue('name')
280            else:
281                attrName = self.__extractName(method)
282
283            # In case the name contains dots, each of the 'domains' but the
284            # last one are translated into nested dictionnaries. For example,
285            # if we want to re-name an attribute into "foo.bar.tofu", the
286            # corresponding fossilized attribute will be of the form:
287            # {"foo":{"bar":{"tofu": res,...},...},...}
288            # instead of:
289            # {"foo.bar.tofu": res, ...}
290
291            current = result
292            attrList = attrName.split('.')
293
294            while len(attrList) > 1:
295                attr = attrList.pop(0)
296                try:
297                    current = current[attr]
298                except KeyError:
299                    current[attr] = {}
300                    current = current[attr]
301
302            # For the last attribute level
303            current[attrList[0]] = methodResult
304
305        if "_type" in result or "_fossil" in result:
306            raise InvalidFossilException('"_type" or "_fossil"'
307                                         ' cannot be a fossil attribute  name')
308        else:
309            result["_type"] = self.__class__.__name__
310            if fossilName: #we check that it's not an empty string
311                result["_fossil"] = fossilName
312            else:
313                result["_fossil"] = ""
314
315        return result
316
317
318def fossilize(target, interfaceArg = None, useAttrCache = False, **kwargs):
319    """
320    Method that allows the "fossilization" process to
321    be called on data structures (lists, dictionaries
322    and sets) as well as normal `Fossilizable` objects.
323
324    :param target: target object to be fossilized
325    :type target: Fossilizable
326    :param interfaceArg: target fossil type
327    :type interfaceArg: IFossil, NoneType, or dict
328    :param useAttrCache: use the attribute caching
329    :type useAttrCache: boolean
330    """
331    return Fossilizable._fossilizeIterable(target, interfaceArg, useAttrCache, **kwargs)
Note: See TracBrowser for help on using the repository browser.