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 20 (checked in by fumanchu, 9 years ago)

1. Added getstate, setstate to Unit and subclasses.
2. Now pickling Units in CachingProxy? to avoid persistent sandbox and other identity issues.

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