| 1 | # -*- test-case-name: twisted.test.test_persisted -*- |
|---|
| 2 | # Copyright (c) Twisted Matrix Laboratories. |
|---|
| 3 | # See LICENSE for details. |
|---|
| 4 | |
|---|
| 5 | |
|---|
| 6 | |
|---|
| 7 | """ |
|---|
| 8 | Different styles of persisted objects. |
|---|
| 9 | """ |
|---|
| 10 | |
|---|
| 11 | # System Imports |
|---|
| 12 | import types |
|---|
| 13 | import copy_reg |
|---|
| 14 | import copy |
|---|
| 15 | import inspect |
|---|
| 16 | import sys |
|---|
| 17 | |
|---|
| 18 | try: |
|---|
| 19 | import cStringIO as StringIO |
|---|
| 20 | except ImportError: |
|---|
| 21 | import StringIO |
|---|
| 22 | |
|---|
| 23 | # Twisted Imports |
|---|
| 24 | from twisted.python import log |
|---|
| 25 | from twisted.python import reflect |
|---|
| 26 | |
|---|
| 27 | oldModules = {} |
|---|
| 28 | |
|---|
| 29 | ## First, let's register support for some stuff that really ought to |
|---|
| 30 | ## be registerable... |
|---|
| 31 | |
|---|
| 32 | def pickleMethod(method): |
|---|
| 33 | 'support function for copy_reg to pickle method refs' |
|---|
| 34 | return unpickleMethod, (method.im_func.__name__, |
|---|
| 35 | method.im_self, |
|---|
| 36 | method.im_class) |
|---|
| 37 | |
|---|
| 38 | def unpickleMethod(im_name, |
|---|
| 39 | im_self, |
|---|
| 40 | im_class): |
|---|
| 41 | 'support function for copy_reg to unpickle method refs' |
|---|
| 42 | try: |
|---|
| 43 | unbound = getattr(im_class,im_name) |
|---|
| 44 | if im_self is None: |
|---|
| 45 | return unbound |
|---|
| 46 | bound = types.MethodType(unbound.im_func, im_self, im_class) |
|---|
| 47 | return bound |
|---|
| 48 | except AttributeError: |
|---|
| 49 | log.msg("Method",im_name,"not on class",im_class) |
|---|
| 50 | assert im_self is not None,"No recourse: no instance to guess from." |
|---|
| 51 | # Attempt a common fix before bailing -- if classes have |
|---|
| 52 | # changed around since we pickled this method, we may still be |
|---|
| 53 | # able to get it by looking on the instance's current class. |
|---|
| 54 | unbound = getattr(im_self.__class__,im_name) |
|---|
| 55 | log.msg("Attempting fixup with",unbound) |
|---|
| 56 | if im_self is None: |
|---|
| 57 | return unbound |
|---|
| 58 | bound = types.MethodType(unbound.im_func, im_self, im_self.__class__) |
|---|
| 59 | return bound |
|---|
| 60 | |
|---|
| 61 | copy_reg.pickle(types.MethodType, |
|---|
| 62 | pickleMethod, |
|---|
| 63 | unpickleMethod) |
|---|
| 64 | |
|---|
| 65 | def pickleModule(module): |
|---|
| 66 | 'support function for copy_reg to pickle module refs' |
|---|
| 67 | return unpickleModule, (module.__name__,) |
|---|
| 68 | |
|---|
| 69 | def unpickleModule(name): |
|---|
| 70 | 'support function for copy_reg to unpickle module refs' |
|---|
| 71 | if oldModules.has_key(name): |
|---|
| 72 | log.msg("Module has moved: %s" % name) |
|---|
| 73 | name = oldModules[name] |
|---|
| 74 | log.msg(name) |
|---|
| 75 | return __import__(name,{},{},'x') |
|---|
| 76 | |
|---|
| 77 | |
|---|
| 78 | copy_reg.pickle(types.ModuleType, |
|---|
| 79 | pickleModule, |
|---|
| 80 | unpickleModule) |
|---|
| 81 | |
|---|
| 82 | def pickleStringO(stringo): |
|---|
| 83 | 'support function for copy_reg to pickle StringIO.OutputTypes' |
|---|
| 84 | return unpickleStringO, (stringo.getvalue(), stringo.tell()) |
|---|
| 85 | |
|---|
| 86 | def unpickleStringO(val, sek): |
|---|
| 87 | x = StringIO.StringIO() |
|---|
| 88 | x.write(val) |
|---|
| 89 | x.seek(sek) |
|---|
| 90 | return x |
|---|
| 91 | |
|---|
| 92 | if hasattr(StringIO, 'OutputType'): |
|---|
| 93 | copy_reg.pickle(StringIO.OutputType, |
|---|
| 94 | pickleStringO, |
|---|
| 95 | unpickleStringO) |
|---|
| 96 | |
|---|
| 97 | def pickleStringI(stringi): |
|---|
| 98 | return unpickleStringI, (stringi.getvalue(), stringi.tell()) |
|---|
| 99 | |
|---|
| 100 | def unpickleStringI(val, sek): |
|---|
| 101 | x = StringIO.StringIO(val) |
|---|
| 102 | x.seek(sek) |
|---|
| 103 | return x |
|---|
| 104 | |
|---|
| 105 | |
|---|
| 106 | if hasattr(StringIO, 'InputType'): |
|---|
| 107 | copy_reg.pickle(StringIO.InputType, |
|---|
| 108 | pickleStringI, |
|---|
| 109 | unpickleStringI) |
|---|
| 110 | |
|---|
| 111 | class Ephemeral: |
|---|
| 112 | """ |
|---|
| 113 | This type of object is never persisted; if possible, even references to it |
|---|
| 114 | are eliminated. |
|---|
| 115 | """ |
|---|
| 116 | |
|---|
| 117 | def __getstate__(self): |
|---|
| 118 | log.msg( "WARNING: serializing ephemeral %s" % self ) |
|---|
| 119 | import gc |
|---|
| 120 | if '__pypy__' not in sys.builtin_module_names: |
|---|
| 121 | if getattr(gc, 'get_referrers', None): |
|---|
| 122 | for r in gc.get_referrers(self): |
|---|
| 123 | log.msg( " referred to by %s" % (r,)) |
|---|
| 124 | return None |
|---|
| 125 | |
|---|
| 126 | def __setstate__(self, state): |
|---|
| 127 | log.msg( "WARNING: unserializing ephemeral %s" % self.__class__ ) |
|---|
| 128 | self.__class__ = Ephemeral |
|---|
| 129 | |
|---|
| 130 | |
|---|
| 131 | versionedsToUpgrade = {} |
|---|
| 132 | upgraded = {} |
|---|
| 133 | |
|---|
| 134 | def doUpgrade(): |
|---|
| 135 | global versionedsToUpgrade, upgraded |
|---|
| 136 | for versioned in versionedsToUpgrade.values(): |
|---|
| 137 | requireUpgrade(versioned) |
|---|
| 138 | versionedsToUpgrade = {} |
|---|
| 139 | upgraded = {} |
|---|
| 140 | |
|---|
| 141 | def requireUpgrade(obj): |
|---|
| 142 | """Require that a Versioned instance be upgraded completely first. |
|---|
| 143 | """ |
|---|
| 144 | objID = id(obj) |
|---|
| 145 | if objID in versionedsToUpgrade and objID not in upgraded: |
|---|
| 146 | upgraded[objID] = 1 |
|---|
| 147 | obj.versionUpgrade() |
|---|
| 148 | return obj |
|---|
| 149 | |
|---|
| 150 | def _aybabtu(c): |
|---|
| 151 | """ |
|---|
| 152 | Get all of the parent classes of C{c}, not including C{c} itself, which are |
|---|
| 153 | strict subclasses of L{Versioned}. |
|---|
| 154 | |
|---|
| 155 | The name comes from "all your base are belong to us", from the deprecated |
|---|
| 156 | L{twisted.python.reflect.allYourBase} function. |
|---|
| 157 | |
|---|
| 158 | @param c: a class |
|---|
| 159 | @returns: list of classes |
|---|
| 160 | """ |
|---|
| 161 | # begin with two classes that should *not* be included in the |
|---|
| 162 | # final result |
|---|
| 163 | l = [c, Versioned] |
|---|
| 164 | for b in inspect.getmro(c): |
|---|
| 165 | if b not in l and issubclass(b, Versioned): |
|---|
| 166 | l.append(b) |
|---|
| 167 | # return all except the unwanted classes |
|---|
| 168 | return l[2:] |
|---|
| 169 | |
|---|
| 170 | class Versioned: |
|---|
| 171 | """ |
|---|
| 172 | This type of object is persisted with versioning information. |
|---|
| 173 | |
|---|
| 174 | I have a single class attribute, the int persistenceVersion. After I am |
|---|
| 175 | unserialized (and styles.doUpgrade() is called), self.upgradeToVersionX() |
|---|
| 176 | will be called for each version upgrade I must undergo. |
|---|
| 177 | |
|---|
| 178 | For example, if I serialize an instance of a Foo(Versioned) at version 4 |
|---|
| 179 | and then unserialize it when the code is at version 9, the calls:: |
|---|
| 180 | |
|---|
| 181 | self.upgradeToVersion5() |
|---|
| 182 | self.upgradeToVersion6() |
|---|
| 183 | self.upgradeToVersion7() |
|---|
| 184 | self.upgradeToVersion8() |
|---|
| 185 | self.upgradeToVersion9() |
|---|
| 186 | |
|---|
| 187 | will be made. If any of these methods are undefined, a warning message |
|---|
| 188 | will be printed. |
|---|
| 189 | """ |
|---|
| 190 | persistenceVersion = 0 |
|---|
| 191 | persistenceForgets = () |
|---|
| 192 | |
|---|
| 193 | def __setstate__(self, state): |
|---|
| 194 | versionedsToUpgrade[id(self)] = self |
|---|
| 195 | self.__dict__ = state |
|---|
| 196 | |
|---|
| 197 | def __getstate__(self, dict=None): |
|---|
| 198 | """Get state, adding a version number to it on its way out. |
|---|
| 199 | """ |
|---|
| 200 | dct = copy.copy(dict or self.__dict__) |
|---|
| 201 | bases = _aybabtu(self.__class__) |
|---|
| 202 | bases.reverse() |
|---|
| 203 | bases.append(self.__class__) # don't forget me!! |
|---|
| 204 | for base in bases: |
|---|
| 205 | if base.__dict__.has_key('persistenceForgets'): |
|---|
| 206 | for slot in base.persistenceForgets: |
|---|
| 207 | if dct.has_key(slot): |
|---|
| 208 | del dct[slot] |
|---|
| 209 | if base.__dict__.has_key('persistenceVersion'): |
|---|
| 210 | dct['%s.persistenceVersion' % reflect.qual(base)] = base.persistenceVersion |
|---|
| 211 | return dct |
|---|
| 212 | |
|---|
| 213 | def versionUpgrade(self): |
|---|
| 214 | """(internal) Do a version upgrade. |
|---|
| 215 | """ |
|---|
| 216 | bases = _aybabtu(self.__class__) |
|---|
| 217 | # put the bases in order so superclasses' persistenceVersion methods |
|---|
| 218 | # will be called first. |
|---|
| 219 | bases.reverse() |
|---|
| 220 | bases.append(self.__class__) # don't forget me!! |
|---|
| 221 | # first let's look for old-skool versioned's |
|---|
| 222 | if self.__dict__.has_key("persistenceVersion"): |
|---|
| 223 | |
|---|
| 224 | # Hacky heuristic: if more than one class subclasses Versioned, |
|---|
| 225 | # we'll assume that the higher version number wins for the older |
|---|
| 226 | # class, so we'll consider the attribute the version of the older |
|---|
| 227 | # class. There are obviously possibly times when this will |
|---|
| 228 | # eventually be an incorrect assumption, but hopefully old-school |
|---|
| 229 | # persistenceVersion stuff won't make it that far into multiple |
|---|
| 230 | # classes inheriting from Versioned. |
|---|
| 231 | |
|---|
| 232 | pver = self.__dict__['persistenceVersion'] |
|---|
| 233 | del self.__dict__['persistenceVersion'] |
|---|
| 234 | highestVersion = 0 |
|---|
| 235 | highestBase = None |
|---|
| 236 | for base in bases: |
|---|
| 237 | if not base.__dict__.has_key('persistenceVersion'): |
|---|
| 238 | continue |
|---|
| 239 | if base.persistenceVersion > highestVersion: |
|---|
| 240 | highestBase = base |
|---|
| 241 | highestVersion = base.persistenceVersion |
|---|
| 242 | if highestBase: |
|---|
| 243 | self.__dict__['%s.persistenceVersion' % reflect.qual(highestBase)] = pver |
|---|
| 244 | for base in bases: |
|---|
| 245 | # ugly hack, but it's what the user expects, really |
|---|
| 246 | if (Versioned not in base.__bases__ and |
|---|
| 247 | not base.__dict__.has_key('persistenceVersion')): |
|---|
| 248 | continue |
|---|
| 249 | currentVers = base.persistenceVersion |
|---|
| 250 | pverName = '%s.persistenceVersion' % reflect.qual(base) |
|---|
| 251 | persistVers = (self.__dict__.get(pverName) or 0) |
|---|
| 252 | if persistVers: |
|---|
| 253 | del self.__dict__[pverName] |
|---|
| 254 | assert persistVers <= currentVers, "Sorry, can't go backwards in time." |
|---|
| 255 | while persistVers < currentVers: |
|---|
| 256 | persistVers = persistVers + 1 |
|---|
| 257 | method = base.__dict__.get('upgradeToVersion%s' % persistVers, None) |
|---|
| 258 | if method: |
|---|
| 259 | log.msg( "Upgrading %s (of %s @ %s) to version %s" % (reflect.qual(base), reflect.qual(self.__class__), id(self), persistVers) ) |
|---|
| 260 | method(self) |
|---|
| 261 | else: |
|---|
| 262 | log.msg( 'Warning: cannot upgrade %s to version %s' % (base, persistVers) ) |
|---|