| 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 |
|---|
| 23 | and lists. Such operation is very useful for generating JSON data structures |
|---|
| 24 | from business objects. It works as a wrapper around `zope.interface`. |
|---|
| 25 | |
|---|
| 26 | Some of the features are: |
|---|
| 27 | * Different "fossil" types for the same source class; |
|---|
| 28 | * Built-in inheritance support; |
|---|
| 29 | """ |
|---|
| 30 | |
|---|
| 31 | import inspect |
|---|
| 32 | import re |
|---|
| 33 | import zope.interface |
|---|
| 34 | from types import NoneType, ClassType, TypeType |
|---|
| 35 | |
|---|
| 36 | |
|---|
| 37 | def fossilizes(*classList): |
|---|
| 38 | """ |
|---|
| 39 | Simple wrapper around 'implements' |
|---|
| 40 | """ |
|---|
| 41 | zope.interface.declarations._implements("fossilizes", |
|---|
| 42 | classList, |
|---|
| 43 | zope.interface.classImplements) |
|---|
| 44 | |
|---|
| 45 | def 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 | |
|---|
| 59 | class NonFossilizableException(Exception): |
|---|
| 60 | """ |
|---|
| 61 | Object is not fossilizable (doesn't implement Fossilizable) |
|---|
| 62 | """ |
|---|
| 63 | |
|---|
| 64 | class WrongFossilTypeException(Exception): |
|---|
| 65 | """ |
|---|
| 66 | Fossil type doesn't apply to target object |
|---|
| 67 | """ |
|---|
| 68 | |
|---|
| 69 | class 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 | |
|---|
| 75 | class IFossil(zope.interface.Interface): |
|---|
| 76 | """ |
|---|
| 77 | Fossil base interface. All fossil classes should derive from this one. |
|---|
| 78 | """ |
|---|
| 79 | |
|---|
| 80 | class 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 | |
|---|
| 318 | def 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) |
|---|