Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

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

root/trunk/__init__.py

Revision 43 (checked in by fumanchu, 8 years ago)

1. Changed UnitProperty?.hints['Size'] to 'bytes'. SM's should now assume infinite bytes unless told otherwise.
2. Abstracted adapters into db.py.
3. Adapter coerce methods now take a coltype arg.
4. Changed safe_name functions to SM.identifier methods.
5. Bugfix: COMPARE_OP now uses op indices not values.
6. Moved len() from CALL_FUNCTION to decompiler.functions.
7. Added db.ConstWrapper? to help with LOAD_CONST corner cases.
8. Added MYSQL SM and test suite.

Line 
1 """Dejavu is an Object-Relational Mapper. This is version 1.2.6.
2
3 Persisted objects are called "Units", and are served into Sandboxes within
4 an Arena. Each Unit instance has a class, which maintains its schema via
5 Unit Properties.
6
7 "Dejavu", to quote Flying Circus episode 16, means "that strange feeling
8 we sometimes get that we've lived through something before." What better
9 name for an object server? Our terminology reflects this cognitive bent:
10 sandboxes "memorize", "recall" and "forget" Units.
11
12 Most Unit lifecycles follow the same pattern:
13     aUnit = sandbox.unit(cls, ID=ID)
14     val = aUnit.propertyName
15     aUnit.propertyName = newValue
16     del aUnit # or otherwise release the reference, e.g. close the scope.
17
18 When creating new Units, a similar pattern would be:
19     newUnit = unit_class()
20     newUnit.propertyName = newValue
21     sandbox.memorize(newUnit)
22     del newUnit # or otherwise release the reference.
23
24 Using recall(), you get an iterator:
25     for unit in sandbox.recall(cls, expr):
26         do_something_with(unit)
27
28 You destroy a Unit via Unit.forget().
29
30 Applications only need to call Unit.repress() when they wish to stop
31 caching the object, returning it to storage. This is very rare, and
32 should really only be performed within dejavu code.
33
34
35 LICENSE
36 -------
37 This work, including the source code, documentation
38 and related data, is placed into the public domain.
39
40 The original author is Robert Brewer, Amor Ministries.
41 fumanchu@amor.org
42 svn://casadeamor.com/dejavu/trunk
43
44 THIS SOFTWARE IS PROVIDED AS-IS, WITHOUT WARRANTY
45 OF ANY KIND, NOT EVEN THE IMPLIED WARRANTY OF
46 MERCHANTABILITY. THE AUTHOR OF THIS SOFTWARE
47 ASSUMES _NO_ RESPONSIBILITY FOR ANY CONSEQUENCE
48 RESULTING FROM THE USE, MODIFICATION, OR
49 REDISTRIBUTION OF THIS SOFTWARE.
50 """
51
52 import ConfigParser
53 import datetime
54 import sha
55 try:
56     import cPickle as pickle
57 except ImportError:
58     import pickle
59
60 from dejavu.containers import *
61 from dejavu.analysis import *
62 from dejavu import logic
63 import xray
64
65
66 ###########################################################################
67 ##                                                                       ##
68 ##                                 Units                                 ##
69 ##                                                                       ##
70 ###########################################################################
71
72
73 # All Units currently must possess an 'ID' UnitProperty. The sequencing of
74 # IDs depends upon their type and the particular needs of the class. Pick
75 # one of these UnitSequencers to fit your subclass.
76
77 # At the moment, no ID sequences are allowed None as a value, since this
78 # signals a Unit which needs to be sequenced when memorized. In addition,
79 # you should aim to create new sequencers which generate IDs that obey
80 # the builtin max() and min() functions.
81
82 class UnitSequencerNull(object):
83     """UnitSequencerNull(type=unicode).
84     A null sequencer for Unit IDs. Sequencing will error.
85     
86     In many cases, ID values simply have no algorithmic sequence;
87     for example, a set of Employee Units might use Social Security
88     Numbers for IDs (which you should never, ever do ;).
89     
90     In other cases, sequencing will be best handled by custom algorithms
91     within application code; that is, the job of abstracting the sequence
92     logic would not be worth the effort.
93     """
94    
95     def __init__(self, type=unicode):
96         self.type = type
97    
98     def next(self, sequence):
99         raise StopIteration("No sequence defined.")
100
101
102 class UnitSequencerInteger(object):
103     """UnitSequencerInteger(type=int, initial=1)
104     A sequencer for Unit IDs, where id[i+1] == id[i] + 1."""
105    
106     def __init__(self, type=int, initial=1):
107         self.type = type
108         self.initial = initial
109    
110     def next(self, sequence):
111         if sequence:
112             m = max(sequence)
113             if m is not None:
114                 return m + 1
115         return self.initial
116
117
118 class UnitSequencerUnicode(object):
119     """UnitSequencerUnicode(type=unicode, width=6,
120         range="abcdefghijklmnopqrstuvwxyz")
121     A sequencer for Unit IDs, where e.g. next(['abc']) == 'abd'."""
122    
123     def __init__(self, type=unicode, width=6,
124                  range="abcdefghijklmnopqrstuvwxyz"):
125         self.type = type
126         self.width = width
127         self.range = range
128    
129     def next(self, sequence):
130         r = self.range
131         if sequence:
132             maxid = max(sequence)
133             if len(maxid) != self.width:
134                 raise OverflowError("'%s' is not of width %s." %
135                                     (maxid, self.width))
136             for i in range(self.width - 1, -1, -1):
137                 pos = r.index(maxid[i]) + 1
138                 if pos >= len(r) or pos < 0:
139                     maxid = maxid[:i] + r[0] + maxid[i+1:]
140                 else:
141                     maxid = maxid[:i] + r[pos] + maxid[i+1:]
142                     break
143             else:
144                 raise OverflowError("Next ID exceeds width %s." % self.width)
145             return maxid
146         return r[0] * self.width
147
148
149 def _define_fixedpoint_states():
150     """Add methods to fixedpoint to support pickling."""
151     import fixedpoint
152    
153     if not hasattr(fixedpoint.FixedPoint, "__getstate__"):
154         def __getstate__(self):
155             return (self.n, self.p)
156         fixedpoint.FixedPoint.__getstate__ = __getstate__
157        
158         def __setstate__(self, v):
159             self.n, self.p = v
160         fixedpoint.FixedPoint.__setstate__ = __setstate__
161
162
163 class UnitProperty(object):
164     """UnitProperty(type=unicode, index=False, hints={}, key=None)
165     Data descriptor for Unit data which will persist in storage.
166     
167     pre, post: Override these with functions to provide custom behaviors
168         upon attribute modification. They are not called if sandbox is None.
169         If you need a behavior which fires regardless, you should override
170         __set__. They also are not called if the new value matches the
171         existing value. Again, override __set__ if you need an exception.
172     
173     hints: A dictionary which provides named hints to Storage Managers
174         concerning the nature of the data. A common use, for example,
175         is to inform Managers that would usually store unicode strings
176         as strings of length 255, that a particular value should be
177         a larger object; this is done with a 'bytes' mapping, such as:
178         hints = {u'bytes': 0}, where 0 implies no limit. Canonical storage
179         hint names and implementation details may be found in /storage
180         documentation.
181     """
182    
183     pre = None
184     post = None
185    
186     def __init__(self, type=unicode, index=False, hints={}, key=None):
187         if type.__name__ == 'FixedPoint':
188             # fixedpoint.Fixedpoint can't be pickled because it
189             # defines __slots__ but not __getstate__. Provide it.
190             _define_fixedpoint_states()
191        
192         self.type = type
193         self.index = index
194         self.hints = hints
195         self.key = key
196    
197     def __get__(self, unit, unitclass=None):
198         if unit is None:
199             # When calling on the class instead of an instance...
200             return self
201         else:
202             return unit._properties[self.key]
203    
204     def __set__(self, unit, value):
205         if self.coerce:
206             value = self.coerce(unit, value)
207        
208         oldvalue = unit._properties[self.key]
209         # This test is expensive, but it saves us a lot of
210         # unnecessary save() operations later on.
211         if value != oldvalue:
212             if unit.sandbox and self.pre:
213                 self.pre(unit, value)
214             unit._properties[self.key] = value
215             if unit.sandbox and self.post:
216                 self.post(unit, value)
217    
218     def coerce(self, unit, value):
219         if value is not None and not isinstance(value, self.type):
220             # Try to coerce the value.
221             try:
222                 value = self.type(value)
223             except Exception, x:
224                 x.args += (value, type(value))
225                 raise x
226         return value
227    
228     def __delete__(self, unit):
229         raise AttributeError("Unit Properties may not be deleted.")
230
231
232 class MetaUnit(type):
233     def __init__(cls, name, bases, dct):
234         cls._associations = {}
235        
236         # Make a copy of the parent class' _properties keys, and store
237         # it in the _properties attribute of this subclass. In this
238         # manner, Unit Properties should propagate down to subclasses,
239         # but not back up to superclasses.
240         props = dict.fromkeys(cls._properties.keys())
241        
242         # Now grab any new UnitProperties defined in this class.
243         # Overwrite any properties defined in superclasses.
244         for key, val in dct.iteritems():
245             if isinstance(val, UnitProperty):
246                 # If the UnitProperty.key is None,
247                 # supply it from the attribute name (key).
248                 if val.key is None:
249                     val.key = key
250                 props[key] = val
251        
252         cls._properties = props
253
254
255 class Unit(object):
256     """Unit(**kwarg properties). A generic, persistent object.
257     
258     Units are the building-block of Dejavu. They are purposefully lightweight,
259     relying on Sandboxes to cache them, which in turn rely on Storage Managers
260     to load and save them.
261     
262     They maintain their own "schema" via UnitProperty objects, so that the
263     Storage Managers don't need to know every detail about every Unit.
264     Storage Managers for simple databases, for example, will simply create
265     a single flat table for each unit type. If you write a custom Storage
266     Manager, you can do as you like; the only place you might run into a
267     problem is if you write a custom Storage Manager for custom Unit types,
268     because the knowledge between the two is indeterminate. For example,
269     if we provide a standard StorageManagerForLotusNotes, and you create
270     custom Units which interface with it, you should probably subclass and
271     extend our StorageManagerForLotusNotes with some custom storage logic.
272     
273     sandbox: The sandbox in which the Unit "lives". Also serves as a flag
274         indicating whether this Unit has finished the initial creation
275         process. While sandbox is None, pre and post descriptor functions
276         will not be called.
277         
278         Sandboxes receive Units during recall() and memorize();
279         these processes should set the sandbox attribute.
280     
281     dirty: indicates whether elements in the _properties dictionary
282         have been modified. This flag is used by Sandboxes to optimize
283         forget(): they do not ask Storage Managers to save data for Units
284         which have not been modified. Because SM's may cache Units, no code
285         should set this flag other than UnitProperty.__set__ and SM's.
286     """
287    
288     __metaclass__ = MetaUnit
289     _properties = {}
290     sequencer = UnitSequencerInteger()
291    
292     # The default ID type is int. If you wish to use a different type for
293     # the ID's of a subclass of Unit, just overwrite ID, e.g.:
294     #     ID = UnitProperty(unicode, index=True)
295     #       or
296     #     UnitSubclass.set_property('ID', unicode, index=True)
297     #       or even
298     #     UnitSubclass.ID.type = unicode
299     # You will probably also want to override Unit.sequencer for the class.
300     ID = UnitProperty(int, index=True)
301    
302     def __init__(self, **kwargs):
303         # Copy the class _properties dict into self, setting
304         # each value to None.
305         self._properties = dict.fromkeys(self.__class__._properties.keys())
306        
307         self.sandbox = None
308        
309         # Make sure we cleanse before assigning properties from kwargs,
310         # or the new unit won't get saved if there are no further changes.
311         self.cleanse()
312         for k, v in kwargs.iteritems():
313             setattr(self, k, v)
314    
315     def _property_hash(self):
316         try:
317             return sha.new(pickle.dumps(self._properties)).digest()
318         except TypeError, x:
319             x.args += (self.__class__.__name__, self._properties.keys())
320             raise x
321    
322     def dirty(self):
323         return self._initial_property_hash != self._property_hash()
324    
325     def cleanse(self):
326         self._initial_property_hash = self._property_hash()
327    
328     def set_property(cls, key, type=unicode, index=False,
329                      descriptor=UnitProperty):
330         """Set a Unit Property for cls."""
331         setattr(cls, key, descriptor(type, index, key=key))
332         cls._properties[key] = None
333     set_property = classmethod(set_property)
334    
335     def set_properties(cls, types={}, descriptor=UnitProperty):
336         """Set Unit Properties for cls."""
337         for key, type in types.items():
338             cls.set_property(key, type, False, descriptor)
339     set_properties = classmethod(set_properties)
340    
341     def indices(cls):
342         """cls.indices() -> tuple of names of indexed UnitProperties."""
343         product = []
344         for key in cls.properties():
345             try:
346                 if getattr(cls, key).index:
347                     product.append(key)
348             except AttributeError, x:
349                 x.args += (cls, key)
350                 raise x
351         return tuple(product)
352     indices = classmethod(indices)
353    
354     def properties(cls):
355         """cls.properties() -> list of UnitProperty names."""
356         return cls._properties.iterkeys()
357     properties = classmethod(properties)
358    
359     def property_type(cls, key):
360         """cls.property_type(key) -> type of the given UnitProperty."""
361         # Retrieving from the class gives us
362         # the UnitProperty object, not its value.
363         return getattr(cls, key).type
364     property_type = classmethod(property_type)
365    
366     def adjust(self, **values):
367         """adjust(**values) -> Set UnitProperties by key, value pairs."""
368         for key, val in values.iteritems():
369             setattr(self, key, val)
370    
371     def repress(self):
372         """repress() -> Remove this Unit from memory (do not destroy)."""
373         self.sandbox.repress(self)
374    
375     def forget(self):
376         """forget() -> Destroy this Unit."""
377         self.sandbox.forget(self)
378    
379     def __copy__(self):
380         newUnit = self.__class__()
381         for key in self.__class__.properties():
382             if key != u'ID':
383                 newUnit._properties[key] = self._properties[key]
384         newUnit.ID = None
385         newUnit.sandbox = None
386         return newUnit
387    
388     def __getstate__(self):
389         return (self._properties, self._initial_property_hash)
390    
391     def __setstate__(self, state):
392         self.sandbox = None
393         self._properties, self._initial_property_hash = state
394    
395    
396     #                        Associations                        #
397    
398     def first(self, farClass, **kwargs):
399         """Return the first associated farClass Unit or None.
400         
401         Passes additional kwargs to sandbox.unit().
402         """
403         try:
404             key, farKey = self.__class__._associations[farClass]
405         except KeyError:
406             raise AssociationError("'%s' is not associated with '%s'"
407                                    % (self.__class__, farClass))
408        
409         value = getattr(self, key)
410         if value is None:
411             return None
412        
413         # kwargs won't take unicode keys
414         kwargs[str(farKey)] = value
415         return self.sandbox.unit(farClass, **kwargs)
416    
417     def add(self, unit):
418         """add(unit) -> Auto-create a relationship between self and unit."""
419         try:
420             key, farKey = self.__class__._associations[unit.__class__]
421         except KeyError:
422             raise AssociationError("'%s' is not associated with '%s'"
423                                    % (self.__class__, unit.__class__))
424        
425         nearval = getattr(self, key)
426         farval = getattr(unit, farKey)
427         if nearval is None:
428             if farval is None:
429                 raise AssociationError("At least one Unit key must be set.")
430             else:
431                 setattr(self, key, farval)
432         else:
433             # If far key is already set, it will simply be overwritten.
434             setattr(unit, farKey, nearval)
435
436
437 def relation_factory(key, farClass, farKey):
438     """Produce a new recaller method for a Unit subclass."""
439     def related_units(self, expr=None):
440         value = getattr(self, key)
441         if value is None:
442             return iter([])
443        
444         # kwargs won't take unicode keys
445         f = logic.filter(**{str(farKey): value})
446         if expr is not None:
447             f += expr
448         return self.sandbox.recall(farClass, f)
449    
450     related_units.__doc__ = (
451     """Iterator over '%(farname)s' Units whose %(farkey)s matches self.%(key)s.
452     If self.%(key)s is None, no Units will be recalled."""
453     % {'farname': farClass.__name__,
454        'farkey': farKey,
455        'key': key,
456        })
457     return related_units
458
459 def associate(cls, key, farClass, farKey, nearFactory=None, farFactory=None):
460     """Associate one Unit class with another by relating attributes.
461     
462     cls, key: The 'near' class and its key.
463     farClass, farKey: the 'far' class and its key.
464     
465     Far Units will be recalled if their farKey matches cls.key.
466     However, if cls.key is empty or None, no Units will be recalled.
467     """
468    
469     # Add a method to cls which retrieves farClass Units
470     if nearFactory is None:
471         nearFactory = relation_factory
472     func = nearFactory(key, farClass, farKey)
473     setattr(cls, farClass.__name__, func)
474    
475     # Add the farClass to the association dictionary of cls.
476     cls._associations[farClass] = (key, farKey)
477    
478     # Add a method to farClass which retrieves cls Units
479     if farFactory is None:
480         farFactory = relation_factory
481     func = farFactory(farKey, cls, key)
482     setattr(farClass, cls.__name__, func)
483    
484     # Add the cls to the association dictionary of farClass.
485     farClass._associations[cls] = (farKey, key)
486
487
488 ###########################################################################
489 ##                                                                       ##
490 ##                                Arenas                                 ##
491 ##                                                                       ##
492 ###########################################################################
493
494
495 class Arena(object):
496     """Arena(). A namespace/workspace for a Dejavu application."""
497    
498     def __init__(self):
499         self.defaultStore = None
500         self.stores = {}
501         self.roster = Prism('name', 'cls', 'store')
502         self.associations = Graph()
503         self.engine_functions = {}
504    
505     def load(self, configFileName):
506         """Load StorageManagers."""
507         parser = ConfigParser.ConfigParser()
508         # Make names case-sensitive by overriding optionxform.
509         parser.optionxform = unicode
510         parser.read(configFileName)
511        
512         stores = []
513         for section in parser.sections():
514             opts = dict(parser.items(section))
515             stores.append((int(opts.get("Load Order", "0")), section, opts))
516         stores.sort()
517        
518         for order, name, options in stores:
519             storage_mgr_class = xray.classes(options[u'Class'])
520             store = storage_mgr_class(name, self, options)
521            
522             unitClasses = []
523             for x in options.get('Units', '').split(","):
524                 clsname = x.strip()
525                 if clsname:
526                     unitClasses.append(clsname)
527            
528             self.add_store(name, store, unitClasses)
529    
530     def add_store(self, name, store, unitClasses=[]):
531         """Register a StorageManager."""
532         self.stores[name] = store
533        
534         # Fill Roster, a Prism of class-associated data.
535         if unitClasses:
536             for clsname in unitClasses:
537                 if clsname:
538                     self.roster.add(name=clsname, cls=None, store=store)
539         else:
540             self.defaultStore = store
541    
542     def shutdown(self):
543         """Shutdown the arena."""
544         # Tell all stores to shut down.
545         stores = [(x.shutdownOrder, x) for x in self.stores.itervalues()]
546         stores.sort()
547         for order, store in stores:
548             store.shutdown()
549    
550     def new_sandbox(self):
551         return Sandbox(self)
552    
553     ###########################################
554     ##        Unit Class Registration        ##
555     ###########################################
556    
557     def register(self, cls):
558         """register(cls) -> Assert that Units of class 'cls' will be handled."""
559         try:
560             row = self.roster.row_number(name=cls.__name__)
561         except ValueError:
562             self.roster.add(name=cls.__name__, cls=cls, store=None)
563         else:
564             # We left cls == None in _load(). Set it now.
565             self.roster.facets['cls'][row] = cls
566        
567         # Register any association(s) in an undirected graph.
568         for farClass in cls._associations:
569             self.associations.connect(cls, farClass)
570    
571     def register_all(self, globals):
572         for obj in globals.itervalues():
573             if isinstance(obj, type) and issubclass(obj, Unit):
574                 self.register(obj)
575    
576     def class_by_name(self, classname):
577         return self.roster.cls(name=classname)
578    
579     def storage(self, cls):
580         s = self.roster.store(cls=cls)
581         if s is None:
582             s = self.defaultStore
583             if s is None:
584                 raise DejavuError("No storage defined for class '%s'" %
585                                   cls.__name__)
586         return s
587    
588     def create_storage(self, cls):
589         self.storage(cls).create_storage(cls)
590
591
592 # Use this arena instance if you are deploying a single application per
593 # process. Otherwise, you should create your own instance per application.
594 dejavuarena = Arena()
595
596
597 ###########################################################################
598 ##                                                                       ##
599 ##                              Sandboxes                                ##
600 ##                                                                       ##
601 ###########################################################################
602
603
604 class Sandbox(object):
605     """Sandbox(arena). Data sandbox for Dejavu arenas.
606     
607     Each consumer (that is, each UI process) maintains a Sandbox for
608     managing Units. Sandboxes populate themselves with Units on a lazy
609     basis, allowing UI code to request data as it's needed. However, once
610     obtained, such Units are persisted (usually for the lifetime of the
611     thread); this important detail means that multiple requests for the
612     same Units result in multiple references to the same objects, rather
613     than multiple objects. Sandboxes are basically what Fowler calls
614     Identity Maps.
615     
616     The *REALLY* important thing to understand if you're customizing this
617     is that Sandboxes won't survive sharing across threads--DON'T TRY IT.
618     If you need to share unit data across requests, use or make an SM which
619     persists the data, and chain it with another, more normal SM.
620     
621     _cache(), _caches, and _stores are private for a reason--don't access
622     them from interface code--tell the Sandbox to do it for you.
623     """
624    
625     def __init__(self, arena):
626         self.arena = arena
627         self._caches = {}
628    
629     def memorize(self, unit):
630         """Attach a unit to this sandbox so that it will persist."""
631         cls = unit.__class__
632         unit.sandbox = self
633        
634         # Ask the store to accept the unit, assigning it an ID if
635         # necessary. The store should also call unit.cleanse()
636         # if it saves the whole unit state on this call.
637         store = self.arena.storage(cls)
638         if store:
639             store.reserve(unit)
640        
641         # Insert the unit into the cache.
642         self._cache(cls)[unit.ID] = unit
643        
644         # Do this at the end of the func, since most on_memorize
645         # will want to have an ID when called.
646         if hasattr(unit, "on_memorize"):
647             unit.on_memorize()
648    
649     def forget(self, unit):
650         """Destroy the unit, both in the cache and storage."""
651         cls = unit.__class__
652         store = self.arena.storage(cls)
653         if store:
654             store.destroy(unit)
655        
656         del self._cache(cls)[unit.ID]
657        
658         if hasattr(unit, "on_forget"):
659             unit.on_forget()
660        
661         unit.sandbox = None
662    
663     def recall(self, cls, expr=None, *args):
664         """recall(cls, expr=None) -> Recall units of cls which match expr.
665         
666         If additional args are supplied:
667         
668         1) They shall be of the form: (cls, expr, cls, expr, cls, [expr]).
669             The final expr is optional.
670         2) Each such secondary cls/expr pair will be recalled; however,
671             only those secondary Units which are associated with Units
672             in the primary set will be returned.
673         3) Instead of single Units, the yielded value will be a tuple of
674             Units, in the same order as the cls args were supplied. This
675             facilitates consumer code like:
676             
677                 for invoice, price in sandbox.recall(Invoice, f, Price):
678                     deal_with(invoice)
679                     deal_with(price)
680             
681         4) If any secondary Units are found, all combinations of those
682             Units, together with each primary Unit, will be returned.
683         5) If no secondary Units are recalled, then a token tuple will be
684             returned of the form (primary_unit, None). For example:
685             
686                 for invoice, price in sandbox.recall(Invoice, f, Price):
687                     if price is None:
688                         handle_no_prices(invoice)
689             
690         """
691        
692         store = self.arena.storage(cls)
693        
694         if args:
695             # Deal with multiple class/expr pairs.
696            
697             # Format extra class/expr pairs more rigorously
698             pairs = []
699             args = list(args)
700             while args:
701                 c = args.pop(0)
702                 if self.arena.storage(c) is not store:
703                     raise ValueError(u"recall() does not currently support "
704                                      u"multiple classes in disparate stores.")
705                 if args:
706                     e = args.pop(0)
707                 else:
708                     e = None
709                 pairs.append((c, e))
710            
711             # Don't try any in-memory techniques, just flush it all,
712             # and ask the SM to give us what we want.
713             self.flush(cls)
714             for c, e in pairs:
715                 self.flush(c)
716             for units in store.recall(cls, expr, pairs):
717                 confirmed = True
718                 for unit in units:
719                     if unit is not None:
720                         ID = unit.ID
721                         cache = self._cache(unit.__class__)
722                         if ID not in cache:
723                             cache[ID] = unit
724                         unit.sandbox = self
725                     if hasattr(unit, 'on_recall'):
726                         try:
727                             unit.on_recall()
728                         except UnrecallableError:
729                             confirmed = False
730                 if confirmed:
731                     yield units
732             raise StopIteration
733        
734         # Run through our cache first.
735         cache = self._cache(cls)
736         skip_cache = False
737        
738         if expr:
739             fc = expr.func.func_code
740             if (fc.co_code == '|\x00\x00i\x01\x00d\x01\x00j\x02\x00S'
741                 and fc.co_names[-1] == 'ID'):
742                 # Special-case the scenario where one Unit is
743                 # expected and called by ID. We should be able
744                 # to save a database hit.
745                 expectedID = fc.co_consts[-1]
746                 if cache.has_key(expectedID):
747                     unit = cache[expectedID]
748                     # Do NOT call on_recall here. That should be called
749                     # only at the Sandbox-SM boundary.
750                     yield unit
751                     raise StopIteration
752                 else:
753                     skip_cache = True
754        
755         if not skip_cache:
756             for unit in cache.itervalues():
757                 if expr is None or expr.evaluate(unit):
758                     # Do NOT call on_recall here. That should be called
759                     # only at the Sandbox-SM boundary.
760                     yield unit
761        
762         # Query Storage.
763         if store:
764             for unit in store.recall(cls, expr):
765                 ID = unit.ID
766                 # Very important that we check for existing unit, as its
767                 # state may have changed in memory but not in storage.
768                 if ID not in cache:
769                     cache[ID] = unit
770                     unit.sandbox = self
771                     confirmed = True
772                     if hasattr(unit, 'on_recall'):
773                         try:
774                             unit.on_recall()
775                         except UnrecallableError:
776                             confirmed = False
777                     if confirmed:
778                         yield unit
779    
780     def unit(self, cls, **kwargs):
781         """Recall a single Unit, else None.
782         
783         **kwargs will be combined into an Expression via logic.filter.
784             The first Unit matching that expression is returned; if no
785             Units match, None is returned.
786         
787         If you need a single Unit which matches a more complex
788             expression, use recall().next().
789         """
790         expr = None
791         if kwargs:
792             expr = logic.filter(**kwargs)
793         try:
794             return self.recall(cls, expr).next()
795         except StopIteration:
796             return None
797    
798     def distinct(self, cls, attrs, expr=None):
799         """Recall distinct Unit property values.
800         
801         If only one attribute is specified, a list of values will be returned.
802         If more than one attribute is specified, a zipped list will be returned.
803         
804         Notice that you can also use this function as a count() function
805         (in fact it's the only way to do it) by using attrs = ['ID'].
806         """
807         seen = {}
808         cache = self._cache(cls)
809         for unit in cache.itervalues():
810             if expr is None or expr.evaluate(unit):
811                 row = tuple([getattr(unit, attr) for attr in attrs])
812                 if row not in seen:
813                     seen[row] = None
814        
815         store = self.arena.storage(cls)
816         if store:
817             for row in store.distinct(cls, attrs, expr):
818                 if row not in seen:
819                     seen[row] = None
820        
821         seen = seen.keys()
822         seen.sort()
823         if len(attrs) == 1:
824             seen = [x[0] for x in seen]
825         return seen
826    
827     def count(self, cls, expr):
828         return len(self.distinct(cls, ['ID'], expr))
829    
830     ####################################
831     ##        Cache Management        ##
832     ####################################
833    
834     def _cache(self, cls):
835         """Return the cache for the specified class.
836         
837         This base class creates a new cache for each cls per request.
838         """
839         if cls not in self._caches:
840             self._caches[cls] = {}
841         return self._caches[cls]
842    
843     def purge(self, cls):
844         del self._caches[cls]
845    
846     def flush(self, cls):
847         """flush(cls) -> Repress all units of the specified class."""
848         cache = self._cache(cls)
849         store = self.arena.storage(cls)
850         while cache:
851             id, unit = cache.popitem()
852            
853             if hasattr(unit, "on_repress"):
854                 unit.on_repress()
855            
856             if store and unit.dirty():
857                 store.save(unit)
858    
859     def flush_all(self):
860         """flush_all() -> repress() all units."""
861         for cls in self._caches.iterkeys():
862             self.flush(cls)
863    
864     def repress(self, unit):
865         """repress(unit) -> Remove unit from cache (but don't destroy)."""
866         if hasattr(unit, "on_repress"):
867             unit.on_repress()
868        
869         cls = unit.__class__
870         store = self.arena.storage(cls)
871         if store and unit.dirty():
872             store.save(unit)
873        
874         del self._cache(cls)[unit.ID]
875
876
877 ###########################################################################
878 ##                                                                       ##
879 ##                               Errors                                  ##
880 ##                                                                       ##
881 ###########################################################################
882
883
884 class DejavuError(Exception):
885     """Base class for errors which occur within Dejavu."""
886     def __init__(self, *args):
887         self.args = args
888    
889     def __str__(self):
890         return u'\n'.join([unicode(eachArg) for eachArg in self.args])
891
892 class AssociationError(DejavuError):
893     """Exception raised when a Unit association fails."""
894     pass
895
896 class UnrecallableError(DejavuError):
897     """Exception raised when a Unit was sought but not recalled."""
898     pass
899
900
901 ###########################################################################
902 ##                                                                       ##
903 ##                           Logic functions                             ##
904 ##                                                                       ##
905 ###########################################################################
906
907
908 def icontains(a, b):
909     """Case-insensitive test b in a. Note the operand order."""
910     if a is None or b is None:
911         return False
912     return b.lower() in a.lower()
913
914 def icontainedby(a, b):
915     """Case-insensitive test a in b. Note the operand order."""
916     if a is None or b is None:
917         return False
918     return a.lower() in b.lower()
919
920 def istartswith(a, b):
921     """True if a starts with b (case-insensitive), False otherwise."""
922     if a is None or b is None:
923         return False
924     return a.lower().startswith(b.lower())
925
926 def iendswith(a, b):
927     """True if a ends with b (case-insensitive), False otherwise."""
928     if a is None or b is None:
929         return False
930     return a.lower().endswith(b.lower())
931
932 def ieq(a, b):
933     """True if a == b (case-insensitive), False otherwise."""
934     if a is None or b is None:
935         return False
936     return (a.lower() == b.lower())
937
938 def year(value):
939     """The year attribute of a date."""
940     if isinstance(value, (datetime.date, datetime.datetime)):
941         return value.year
942     else:
943         return None
944
945 def now():
946     """Late-bound datetime.datetime.now(). Taint this when early binding."""
947     return datetime.datetime.now()
948 now.bind_late = True
949
950 def today():
951     """Late-bound datetime.date.today(). Taint this when early binding."""
952     return datetime.date.today()
953 today.bind_late = True
954
955 def iscurrentweek(value):
956     """If value is in the current week, return True, else False."""
957     if isinstance(value, (datetime.date, datetime.datetime)):
958         return datetime.date.today().strftime('%W%Y') == value.strftime('%W%Y')
959     else:
960         return False
961 iscurrentweek.bind_late = True
962
963 # Inject these functions into the logic module's globals.
964 class _Empty(object): pass
965 _d = _Empty()
966 for _name in ['icontains', 'icontainedby', 'istartswith', 'iendswith',
967               'ieq', 'year', 'now', 'today', 'iscurrentweek']:
968     setattr(_d, _name, globals()[_name])
969 logic.dejavu = _d
Note: See TracBrowser for help on using the browser.