Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

I think I've seen this ORM somewhere before...

root/branches/ldap/arenas.py

Revision 397 (checked in by fumanchu, 6 years ago)

Privatized Sandbox.xmulti and wrote a bunch of docstrings.

  • Property svn:eol-style set to native
Line 
1
2 import ConfigParser
3 try:
4     set
5 except NameError:
6     from sets import Set as set
7 import threading
8 from types import ClassType
9
10 from dejavu.containers import Graph
11 from dejavu import logic, errors, storage, xray
12
13 __all__ = ['Arena', 'Sandbox', 'logflags',
14            ]
15
16
17 class Enum(object):
18     pass
19
20 # logging flags (see Arena.logflags)
21 logflags = Enum()
22 logflags.ERROR = 1
23 logflags.IO = 2
24 logflags.SQL = 4
25
26 logflags.MEMORIZE = 128
27 logflags.RECALL = 256
28 logflags.VIEW = 512
29 logflags.REPRESS = 1024
30 logflags.FORGET = 2048
31 logflags.SANDBOX = (logflags.MEMORIZE | logflags.RECALL | logflags.VIEW |
32                     logflags.REPRESS | logflags.FORGET)
33
34
35 class Arena(object):
36     """A namespace/workspace for a Dejavu application."""
37    
38     def __init__(self):
39         self.stores = {}
40         self._registered_classes = {}
41         self.associations = Graph(directed=False)
42         self.engine_functions = {}
43         self.logflags = logflags.ERROR + logflags.IO
44    
45     def log(self, message):
46         """Default logger (writes to stdout). Feel free to replace."""
47         if isinstance(message, unicode):
48             print message.encode('utf8')
49         else:
50             print message
51    
52     def load(self, configFileName):
53         """Load StorageManagers from the given filename."""
54         parser = ConfigParser.ConfigParser()
55         # Make names case-sensitive by overriding optionxform.
56         parser.optionxform = unicode
57         parser.read(configFileName)
58        
59         stores = []
60         for section in parser.sections():
61             opts = dict(parser.items(section))
62             stores.append((int(opts.get("Load Order", "0")), section, opts))
63         stores.sort()
64        
65         for order, name, options in stores:
66             self.add_store(name, options[u'Class'], options)
67    
68     def add_store(self, name, store, options=None):
69         """Register a StorageManager.
70         
71         The 'store' argument may be a StorageManager class, an instance of
72         that class, or the full importable dotted-package name of the class.
73         """
74        
75         if isinstance(store, basestring):
76             if store in storage.managers:
77                 store = storage.managers[store]
78        
79         if isinstance(store, basestring):
80             store = xray.classes(store)(self, options or {})
81         elif isinstance(store, (type, ClassType)):
82             store = store(self, options or {})
83        
84         self.stores[name] = store
85         return store
86    
87     def remove_store(self, name):
88         """Remove (unregister) the named store.
89         
90         All classes associated to the given store will be disassociated,
91         and will then be free to associate with another store (usually
92         the default store).
93         """
94         if name in self.stores:
95             store = self.stores[name]
96            
97             # Disassociate all registered classes with this store.
98             for c in self._registered_classes.keys():
99                 if self._registered_classes[c] is store:
100                     self._registered_classes[c] = None
101            
102             del self.stores[name]
103    
104     def map_all(self):
105         """Map all registered classes to internal storage structures.
106         
107         Although classes are mapped automatically the first time they
108         are accessed (see Arena.storage), in production it is often more
109         useful to map all classes at application startup. Call this
110         method to do so (but register all classes first).
111         
112         This method is idempotent, but that doesn't mean cheap. Try not
113         to call it very often (once at app startup is usually enough).
114         """
115         storemap = {}
116        
117         # Don't use iteritems because self.storage may mutate the dict.
118         for cls, store in self._registered_classes.items():
119             if store is None:
120                 # Call _find_storage directly to skip the map()
121                 # call in Arena.storage.
122                 store = self._find_storage(cls)
123                 self._registered_classes[cls] = store
124             bucket = storemap.setdefault(store, [])
125             bucket.append(cls)
126        
127         storemap = [(s.loadOrder, s, c) for s, c in storemap.iteritems()]
128         storemap.sort()
129         for order, store, classes in storemap:
130             store.map(classes)
131    
132     def shutdown(self):
133         """Shutdown the arena and all its stores."""
134         # Tell all stores to shut down.
135         stores = [(v.shutdownOrder, v, k) for k, v in self.stores.iteritems()]
136         stores.sort()
137         for order, store, name in stores:
138             store.shutdown()
139    
140     def new_sandbox(self):
141         """Return a new sandbox object in this Arena."""
142         return Sandbox(self)
143    
144    
145     # --------------------- Unit Class Registration --------------------- #
146    
147     def register(self, cls):
148         """Assert that Units of class 'cls' will be handled."""
149         # We must allow modules to register classes before any stores have
150         # been added, but not overwrite a store which has already been found.
151         if cls not in self._registered_classes:
152             self._registered_classes[cls] = None
153            
154             # Register any association(s) in an undirected graph.
155             for ua in cls._associations.itervalues():
156                 if getattr(ua, "register", True):
157                     self.associations.connect(cls, ua.farClass)
158    
159     def register_all(self, globals):
160         """Register each subclass of Unit in the given globals."""
161         import dejavu
162         seen = {}
163         for obj in globals.itervalues():
164             if isinstance(obj, type) and issubclass(obj, dejavu.Unit):
165                 self.register(obj)
166                 seen[obj] = None
167         return seen.keys()
168    
169     def class_by_name(self, classname):
170         """Return the class object for the given classname."""
171         for cls in self._registered_classes:
172             if cls.__name__ == classname:
173                 return cls
174         raise KeyError("No registered class found for '%s'." % classname)
175    
176     def storage(self, cls):
177         """Return the StorageManager which handles Units of the given class.
178         
179         The results of this call will be cached for performance. Also, this
180         will call store.map(cls) on first retrieval.
181         """
182         store = self._registered_classes.get(cls)
183         if store:
184             return store
185        
186         store = self._find_storage(cls)
187         self._registered_classes[cls] = store
188         store.map([cls])
189         return store
190    
191     def _find_storage(self, cls):
192         """Find the Storage Manager which handles Units of the given class.
193         
194         This method (unlike the 'storage' method) does no caching or mapping.
195         """
196         # Search all stores for the class name.
197         default_store = None
198         clsname = cls.__name__
199         for store in self.stores.values():
200             if not store.classnames:
201                 # This store has no "classnames" list, which signals that it
202                 # handles all classes which are not handled by other stores.
203                 default_store = store
204             elif clsname in store.classnames:
205                 return store
206        
207         # Name not found in any store's classnames. Try a default store.
208         if default_store:
209             return default_store
210        
211         raise KeyError("No store found for '%s'." % clsname)
212    
213     def create_storage(self, cls):
214         """Create storage space for cls."""
215         # Skip the map() call inside storage().
216         store = self._registered_classes.get(cls)
217         if not store:
218             store = self._find_storage(cls)
219             self._registered_classes[cls] = store
220         store.create_storage(cls)
221    
222     def has_storage(self, cls):
223         """If storage space for cls exists, return True (False otherwise)."""
224         try:
225             return self.storage(cls).has_storage(cls)
226         except errors.MappingError:
227             return False
228    
229     def drop_storage(self, cls):
230         """Remove storage space for cls."""
231         self.storage(cls).drop_storage(cls)
232    
233     def add_property(self, cls, name):
234         """Add storage space for the named property of the given cls."""
235         self.storage(cls).add_property(cls, name)
236    
237     def drop_property(self, cls, name):
238         """Drop storage space for the named property of the given cls."""
239         self.storage(cls).drop_property(cls, name)
240    
241     def rename_property(self, cls, oldname, newname):
242         """Rename storage space for the property of the given cls."""
243         self.storage(cls).rename_property(cls, oldname, newname)
244    
245     def migrate_class(self, cls, new_store):
246         """Copy all units of cls to new_store."""
247         new_store.create_storage(cls)
248         for unit in self.new_sandbox().xrecall(cls):
249             new_store.reserve(unit)
250             new_store.save(unit, True)
251    
252     def migrate(self, new_store, old_store=None, copy_only=False):
253         """Copy all units (of old_store) to new_store."""
254         for cls in self._registered_classes:
255             store = self.storage(cls)
256             if old_store is None or old_store is store:
257                 self.migrate_class(cls, new_store)
258                 if not copy_only:
259                     self._registered_classes[cls] = new_store
260
261
262 ###########################################################################
263 ##                                                                       ##
264 ##                              Sandboxes                                ##
265 ##                                                                       ##
266 ###########################################################################
267
268
269 class Sandbox(object):
270     """Data sandbox for Dejavu arenas.
271     
272     Each consumer (that is, each UI process or thread) maintains a Sandbox
273     for managing Units. Sandboxes populate themselves with Units on a lazy
274     basis, allowing UI code to request data as it's needed. However, once
275     obtained, such Units are persisted (usually for the lifetime of the
276     thread); this important detail means that multiple requests for the
277     same Units result in multiple references to the same objects, rather
278     than multiple objects. Sandboxes are basically what Fowler calls
279     Identity Maps.
280     
281     The *REALLY* important thing to understand if you're customizing this
282     is that Sandboxes won't survive sharing across threads--DON'T TRY IT.
283     If you need to share unit data across requests, use or make an SM which
284     persists the data, and chain it with another, more normal SM.
285     
286     _cache(), _caches, and _stores are private for a reason--don't access
287     them from interface code--tell the Sandbox to do it for you.
288     
289     Starting with Python 2.5, each Sandbox instance is its own context
290     manager, so you can have boxes automatically flush themselves
291     when you're done, and automatically rollback on error. Example:
292     
293     # __future__ only needed for Python 2.5, not 2.6+
294     from __future__ import with_statement
295     
296     with arena.new_sandbox() as box:
297         WAP = box.unit(Zoo, Name='Wild Animal Park')
298         WAP.Opens = now
299     """
300    
301     def __init__(self, arena):
302         self.arena = arena
303         self._caches = {}
304    
305     def __getattr__(self, key):
306         # Support "magic recaller" methods on self.
307         for cls in self.arena._registered_classes.iterkeys():
308             name = cls.__name__
309             if name == key:
310                 def recaller(*args, **kwargs):
311                     # Allow identifiers to be supplied as args or kwargs
312                     # (since the common case will be a single identifier).
313                     for arg, key in zip(args, cls.identifiers):
314                         kwargs[str(key)] = arg
315                     expr = logic.filter(**kwargs)
316                     try:
317                         return self.xrecall(cls, expr).next()
318                     except StopIteration:
319                         return None
320                 recaller.__doc__ = "A single %s Unit, else None." % name
321                 return recaller
322         raise AttributeError("Sandbox object has no attribute '%s'" % key)
323    
324     def memorize(self, unit):
325         """Persist the given unit in storage."""
326         cls = unit.__class__
327         unit.sandbox = self
328        
329         # Ask the store to accept the unit, assigning it primary key values
330         # if necessary. The store should also call unit.cleanse() if it
331         # saves the whole unit state on this call.
332         self.arena.storage(cls).reserve(unit)
333        
334         # Insert the unit into the cache.
335         id = unit.identity()
336         self._cache(cls)[id] = unit
337         if self.arena.logflags & logflags.MEMORIZE:
338             self.arena.log("MEMORIZE %s: %s" % (cls.__name__, id))
339        
340         # Do this at the end of the func, since most on_memorize
341         # will want to have an identity when called.
342         if hasattr(unit, "on_memorize"):
343             unit.on_memorize()
344    
345     def forget(self, unit):
346         """Destroy the given unit, both in the cache and storage."""
347         cls = unit.__class__
348        
349         id = unit.identity()
350         if self.arena.logflags & logflags.FORGET:
351             self.arena.log("FORGET %s: %s" % (cls.__name__, id))
352         self.arena.storage(cls).destroy(unit)
353        
354         del self._cache(cls)[id]
355        
356         # This must be done after the destroy() call, so that a
357         # related unit can poll all instances of this class.
358         if hasattr(unit, "on_forget"):
359             unit.on_forget()
360        
361         unit.sandbox = None
362    
363     def xrecall(self, classes, expr=None, inherit=False, **kwargs):
364         """Iterator over units of the given class(es) which match expr.
365         
366         If inherit is True, units of the given class and all registered
367         subclasses of the given class will be recalled.
368         
369         If the 'classes' arg is a UnitJoin, each yielded value will be a
370         list of Units, in the same order as the classes arg.
371         This facilitates unpacking in iterative consumer code like:
372         
373         for invoice, price in sandbox.xrecall(Invoice & Price, f):
374             deal_with(invoice)
375             deal_with(price)
376         
377         Recalling multiple classes is currently not well-isolated.
378         If an expr argument is supplied, then the store may not return rows
379         which our cache would, and those won't be included in the resultset.
380         If you're using xrecall with joins, you should be safe if:
381         
382             * You pass no expr, or
383             * You're using this sandbox as read-only, or
384             * You call flush_all() after mutating Units but before recalling
385                 multiple classes.
386         
387         This also does not yet support multiple classes in separate stores.
388         """
389         if classes.__class__.__name__ == "UnitJoin":
390             for unitrow in self._xmulti(classes, expr, **kwargs):
391                 yield unitrow
392             return
393        
394         cls = classes
395        
396         if expr and not isinstance(expr, logic.Expression):
397             expr = logic.Expression(expr)
398         if kwargs:
399             f = logic.filter(**kwargs)
400             if expr:
401                 expr += f
402             else:
403                 expr = f
404        
405         if self.arena.logflags & logflags.RECALL:
406             self.arena.log("RECALL %s: %s" % (cls.__name__, expr))
407        
408         if inherit:
409             # Collect all registered subclasses of cls.
410             # Note that cls is a subclass of itself.
411             classes = [c for c in self.arena._registered_classes.iterkeys()
412                        if issubclass(c, cls)]
413         else:
414             classes = [cls]
415         if not classes:
416             # Even the requested class is not registered.
417             raise errors.UnrecallableError("The '%s' class is not registered."
418                                            % cls.__name__)
419        
420         for cls in classes:
421             cache = self._cache(cls)
422            
423             # Special-case the scenario where one Unit is expected
424             # and called by ID. We should be able to save a database hit.
425             if expr:
426                 fc = expr.func.func_code
427                 if (fc.co_code == '|\x00\x00i\x01\x00d\x01\x00j\x02\x00S'
428                     and fc.co_names[-1] == 'ID'):
429                     ID = fc.co_consts[-1]
430                     unit = cache.get((ID,))
431                     if unit is not None:
432                         # Do NOT call on_recall here. That should be called
433                         # only at the Sandbox-SM boundary.
434                         yield unit
435                         return
436            
437             # Query the cache. We have to use a static copy of the
438             # keys, to ensure that our cache doesn't change size
439             # during iteration (due to overlapping xrecalls).
440             keys = cache.keys()
441             for id in keys:
442                 unit = cache.get(id)
443                 if unit and ((expr is None) or expr.evaluate(unit)):
444                     # Do NOT call on_recall here. That should be called
445                     # only at the Sandbox-SM boundary.
446                     yield unit
447            
448             # Query Storage.
449             for unit in self.arena.storage(cls).recall(cls, expr):
450                 id = unit.identity()
451                 # Don't offer up a unit that was already checked in our cache
452                 # (whether it matched the expr() or not--we assume the cache
453                 # has the freshest data).
454                 if id not in keys:
455                     # Very important that we check for existing unit, as its
456                     # state may have changed in memory but not in storage
457                     # (even between our cache yields and this yield).
458                     # Make sure the cache lookup and get happens atomically.
459                     existing = cache.get(id)
460                     if existing:
461                         yield existing
462                     else:
463                         unit.sandbox = self
464                         confirmed = True
465                         cache[id] = unit
466                         if hasattr(unit, 'on_recall'):
467                             try:
468                                 unit.on_recall()
469                             except errors.UnrecallableError:
470                                 confirmed = False
471                         if confirmed:
472                             yield unit
473    
474     def recall(self, classes, expr=None, inherit=False, **kwargs):
475         """List of units of the given class(es) which match expr.
476         
477         If inherit is True, units of the given class and all registered
478         subclasses of the given class will be recalled.
479         
480         If the 'classes' arg is a UnitJoin, each yielded value will be a
481         list of Units, in the same order as the classes arg.
482         This facilitates unpacking in iterative consumer code like:
483         
484         for invoice, price in sandbox.recall(Invoice & Price, f):
485             deal_with(invoice)
486             deal_with(price)
487         
488         Recalling multiple classes is currently not well-isolated.
489         If an expr argument is supplied, then the store may not return rows
490         which our cache would, and those won't be included in the resultset.
491         If you're using recall with joins, you should be safe if:
492         
493             * You pass no expr, or
494             * You're using this sandbox as read-only, or
495             * You call flush_all() after mutating Units but before recalling
496                 multiple classes.
497         
498         This also does not yet support multiple classes in separate stores.
499         """
500         return [x for x in self.xrecall(classes, expr, inherit, **kwargs)]
501    
502     def _xmulti(self, classes, expr=None, **kwargs):
503         """Recall units of each cls if they together match the expr.
504         
505         The 'classes' arg must be a UnitJoin, and each yielded value
506         will be a list of Units, in the same order as the classes arg.
507         This facilitates unpacking in iterative consumer code like:
508         
509         for invoice, price in sandbox.recall(Invoice & Price, f):
510             deal_with(invoice)
511             deal_with(price)
512         
513         Recalling multiple classes is currently not well-isolated.
514         If an expr argument is supplied, then the store may not return rows
515         which our cache would, and those won't be included in the resultset.
516         If you're using recall with joins, you should be safe if:
517         
518             * You pass no expr, or
519             * You're using this sandbox as read-only, or
520             * You call flush_all() after mutating Units but before recalling
521                 multiple classes.
522         
523         This also does not yet support multiple classes in separate stores.
524         """
525        
526         if expr and not isinstance(expr, logic.Expression):
527             expr = logic.Expression(expr)
528         if kwargs:
529             f = logic.filter(**kwargs)
530             if expr:
531                 expr += f
532             else:
533                 expr = f
534        
535         if self.arena.logflags & logflags.RECALL:
536             self.arena.log("RECALL %s %s" %
537                            (", ".join([c.__name__ for c in classes]), expr))
538        
539         stores = [self.arena.storage(cls) for cls in classes]
540         firststore = stores[0]
541         for s in stores:
542             if s is not firststore:
543                 raise ValueError(u"(x)recall does not support multiple"
544                                  u" classes in disparate stores.")
545        
546         # This is broken. If a filter expr is supplied, then the store may
547         # not return rows which our cache would, and those won't be included
548         # in the resultset. If you're using xmulti with no expr's, or
549         # in read-only scripts, it should be OK for now. But if you mutate
550         # Units and then call multirecall, expect inconsistent results.
551         for unitset in firststore.multirecall(classes, expr):
552             confirmed = True
553             for index in xrange(len(unitset)):
554                 unit = unitset[index]
555                 id = unit.identity()
556                 cache = self._cache(unit.__class__)
557                 if id in cache:
558                     # Keep the unit which is in our cache!
559                     unitset[index] = cache[id]
560                 else:
561                     cache[id] = unit
562                     unit.sandbox = self
563                     if hasattr(unit, 'on_recall'):
564                         try:
565                             unit.on_recall()
566                         except errors.UnrecallableError:
567                             confirmed = False
568                             break
569             if confirmed:
570                 yield unitset
571    
572     def unit(self, cls, expr=None, inherit=False, **kwargs):
573         """A single Unit which matches the given expr, else None.
574         
575         If inherit is True, the given class and all registered subclasses
576         of the given class will be searched for a matching Unit.
577         
578         **kwargs will be combined into an Expression via logic.filter.
579         The first Unit matching that expression is returned; if no
580         Units match, None is returned.
581         """
582         try:
583             return self.xrecall(cls, expr, inherit, **kwargs).next()
584         except StopIteration:
585             return None
586    
587     def view(self, cls, attrs, expr=None, **kwargs):
588         """Yield tuples of attrs for the given cls which match the expr.
589         
590         cls: The Unit subclass for which to yield property tuples.
591         attrs: a sequence of strings; each should be the name of
592             a UnitProperty on the given cls.
593         expr: a lambda or logic.Expression. If provided, data will only
594             be yielded for units of the given cls which match the expr.
595         **kwargs: additional expr filters in name=value format.
596         
597         Each yielded value will be a list of values, in the same order as
598         the attrs arg. This facilitates unpacking in iterative consumer
599         code like:
600         
601         for id, name in sandbox.view(Invoice, ['ID', 'Name'], f):
602             print id, ": ", name
603         
604         This is generally much faster than recall, and should be preferred
605         for performance-sensitive code.
606         """
607         if expr and not isinstance(expr, logic.Expression):
608             expr = logic.Expression(expr)
609         if kwargs:
610             f = logic.filter(**kwargs)
611             if expr:
612                 expr += f
613             else:
614                 expr = f
615        
616         if self.arena.logflags & logflags.VIEW:
617             self.arena.log("VIEW %s [%s]: %s" % (cls.__name__, attrs, expr))
618        
619         cache = self._cache(cls)
620        
621         for unit in cache.itervalues():
622             if expr is None or expr(unit):
623                 yield tuple([getattr(unit, attr) for attr in attrs])
624        
625         # Add the identity attribute(s) if not present. This is necessary
626         # to avoid duplicating objects which are already in our cache.
627         fields = list(attrs)
628         indices = []
629         added_fields = 0
630         for key in cls.identifiers:
631             if key not in fields:
632                 added_fields += 1
633                 fields.append(key)
634             indices.append(fields.index(key))
635        
636         for row in self.arena.storage(cls).view(cls, fields, expr):
637             id = tuple([row[x] for x in indices])
638             if id not in cache:
639                 if added_fields:
640                     # Remove the added identifier columns from the row.
641                     row = row[:-added_fields]
642                 yield row
643    
644     def sum(self, cls, attr, expr=None, **kwargs):
645         """Sum of all non-None values for the given cls.attr."""
646         expr = logic.Expression(lambda x: getattr(x, attr) != None) + expr
647         return sum([row[0] for row in self.view(cls, (attr,), expr, **kwargs)])
648    
649     def distinct(self, cls, attrs, expr=None, **kwargs):
650         """List of distinct UnitProperty tuples for the given cls.
651         
652         If only one attribute is specified, a list of values will be returned.
653         If more than one attribute is specified, a zipped list will be returned.
654         
655         Notice that you can also use this function as a count() function
656         (in fact it's the only way to do it) by using attrs = ['ID'].
657         """
658         if expr and not isinstance(expr, logic.Expression):
659             expr = logic.Expression(expr)
660         if kwargs:
661             f = logic.filter(**kwargs)
662             if expr:
663                 expr += f
664             else:
665                 expr = f
666        
667         if self.arena.logflags & logflags.VIEW:
668             self.arena.log("DISTINCT %s [%s]: %s" % (cls.__name__, attrs, expr))
669        
670         seen = {}
671         cache = self._cache(cls)
672         for unit in cache.itervalues():
673             if expr is None or expr(unit):
674                 row = tuple([getattr(unit, attr) for attr in attrs])
675                 if row not in seen:
676                     seen[row] = None
677        
678         for row in self.arena.storage(cls).distinct(cls, attrs, expr):
679             if row not in seen:
680                 seen[row] = None
681        
682         seen = seen.keys()
683         seen.sort()
684         if len(attrs) == 1:
685             seen = [x[0] for x in seen]
686         return seen
687    
688     def count(self, cls, expr):
689         """Number of Units of the given cls which match the given expr."""
690         return len(self.distinct(cls, cls.identifiers, expr))
691    
692     def range(self, cls, attr, expr=None, **kwargs):
693         """Distinct, non-None attr values (ordered and continuous, if possible).
694         
695         If the given attribute is a known discrete, ordered type
696         (like int, long, datetime.date), this returns the closed interval:
697             
698             [min(attr), ..., max(attr)]
699         
700         That is, all possible values will be output between min and max,
701         even if they do not appear in the dataset.
702         
703         If the given attribute is not reasonably discrete (e.g., str,
704         unicode, or float) then all distinct, non-None values are returned
705         (sorted, if possible).
706         """
707         existing = [x for x in self.distinct(cls, [attr], expr, **kwargs)
708                     if x is not None]
709         if not existing:
710             return []
711        
712         attr_type = getattr(cls, attr).type
713         if issubclass(attr_type, (int, long)):
714             return range(min(existing), max(existing) + 1)
715         else:
716             try:
717                 import datetime
718             except ImportError:
719                 pass
720             else:
721                 if issubclass(attr_type, datetime.date):
722                     def date_gen():
723                         start, end = min(existing), max(existing)
724                         for d in range((end + 1) - start):
725                             yield start + datetime.timedelta(d)
726                     return date_gen()
727        
728         try:
729             existing.sort()
730         except TypeError:
731             pass
732        
733         return existing
734    
735     #                           Cache Management                           #
736    
737     def _cache(self, cls):
738         """Return the cache for the specified class.
739         
740         This base class creates a new cache for each cls per request.
741         """
742         if cls not in self._caches:
743             self._caches[cls] = {}
744         return self._caches[cls]
745    
746     def purge(self, cls):
747         """Drop all cached Units of class 'cls'. Do not save."""
748         del self._caches[cls]
749    
750     def repress(self, unit):
751         """Remove unit from cache (but don't destroy)."""
752         cls = unit.__class__
753         id = unit.identity()
754         if self.arena.logflags & logflags.REPRESS:
755             self.arena.log("REPRESS %s: %s" % (cls.__name__, id))
756        
757         if hasattr(unit, "on_repress"):
758             unit.on_repress()
759        
760         # Save after on_repress in case on_repress modified the unit.
761         self.arena.storage(cls).save(unit)
762        
763         del self._cache(cls)[id]
764         unit.sandbox = None
765    
766     def flush_all(self):
767         """Repress all units and commit any open transaction."""
768        
769         for cls in self._caches.keys():
770             # Call all on_repress methods first! There are truly horrible
771             # interdependency chains in most on_repress methods, and
772             # it's best to resolve them all at once BEFORE flushing
773             # any units from the cache.
774             # Note we use values instead of itervalues, since the
775             # cache may change size during iteration.
776             for unit in self._cache(cls).values():
777                 if hasattr(unit, "on_repress"):
778                     unit.on_repress()
779        
780         seen_stores = {}
781         for cls in self._caches.keys():
782             cache = self._cache(cls)
783             store = self.arena.storage(cls)
784             seen_stores[store] = None
785             while cache:
786                 unitid, unit = cache.popitem()
787                 if self.arena.logflags & logflags.REPRESS:
788                     self.arena.log("REPRESS %s: %s" % (cls.__name__, unitid))
789                 store.save(unit)
790        
791         self.commit()
792    
793     #                        Transaction Management                        #
794    
795     def start(self, isolation=None):
796         """Start a transaction."""
797         for store in set(self.arena._registered_classes.values()):
798             # If store is None, the class was never mapped to a store.
799             if store:
800                 # By default, stores do not support transactions,
801                 # in which case 'start' will be None.
802                 if store.start:
803                     store.start(isolation)
804    
805     def commit(self):
806         """Commit the current transaction.
807         
808         If errors occur during this process, they are not trapped here.
809         You must either call rollback yourself (or fix the problem and
810         try to commit again).
811         """
812         for store in set(self.arena._registered_classes.values()):
813             if store and store.commit:
814                 store.commit()
815    
816     def rollback(self):
817         """Roll back the current transaction (all changes) and purge our cache."""
818         for cls in self._caches.keys():
819             # Dump all objects in this cache
820             self.purge(cls)
821        
822         for store in set(self.arena._registered_classes.values()):
823             if store and store.rollback:
824                 store.rollback()
825    
826     #                          Context Management                          #
827    
828     def __enter__(self):
829         return self
830    
831     def __exit__ (self, type, value, tb):
832         if tb is None:
833             self.flush_all()
834         else:
835             self.rollback()
836
Note: See TracBrowser for help on using the browser.