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

1. Moved xray, recur into dejavu.
2. Doc updates.

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