Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

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

Changeset 101

Show
Ignore:
Timestamp:
11/20/05 23:23:41
Author:
fumanchu
Message:

Fix for #20 (awareness of to-one or to-many).

  1. Magic Unit relation methods may now return a single Unit (or None) if they are to-one. To-many methods still return a list.
  2. dejavu.associate is replaced by Unit.associate, one_to_many, one_to_one, and many_to_one.
  3. Unit.first is removed.
  4. One-way associations are now possible via dejavu.ToOne? and .ToMany?. Customization of relation descriptors is easier (override UnitAssociation?.related).
  5. Unit._associations dict has changed protocol.
  6. Lots of module breakouts to make reading easier.
Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/__init__.py

    r100 r101  
    3131caching the object, returning it to storage. This is very rare, and 
    3232should really only be performed within dejavu code. 
     33""" 
     34 
     35__version__ =  "1.4 beta" 
    3336 
    3437 
    35 LICENSE 
    36 ------- 
    37 This work, including the source code, documentation 
    38 and related data, is placed into the public domain. 
     38import datetime 
    3939 
    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 __version__ =  "1.3 beta 2" 
    53  
    54  
    55 import ConfigParser 
    56 import datetime 
    57 import sha 
    58 try: 
    59     import cPickle as pickle 
    60 except ImportError: 
    61     import pickle 
    62  
     40from dejavu.analysis import * 
     41from dejavu.arenas import * 
    6342from dejavu.containers import * 
    64 from dejavu.analysis import * 
     43from dejavu.errors import * 
     44from dejavu.units import * 
    6545from dejavu import logic 
    66  
    67  
    68 ########################################################################### 
    69 ##                                                                       ## 
    70 ##                                 Units                                 ## 
    71 ##                                                                       ## 
    72 ########################################################################### 
    73  
    74  
    75 # All Units currently must possess an 'ID' UnitProperty. The sequencing of 
    76 # IDs depends upon their type and the particular needs of the class. Pick 
    77 # one of these UnitSequencers to fit your subclass. 
    78  
    79 # At the moment, no ID sequences are allowed None as a value, since this 
    80 # signals a Unit which needs to be sequenced when memorized. In addition, 
    81 # you should aim to create new sequencers which generate IDs that obey 
    82 # the builtin max() and min() functions. 
    83  
    84 class UnitSequencerNull(object): 
    85     """UnitSequencerNull(type=unicode). 
    86     A null sequencer for Unit IDs. Sequencing will error. 
    87      
    88     In many cases, ID values simply have no algorithmic sequence; 
    89     for example, a set of Employee Units might use Social Security 
    90     Numbers for IDs (which you should never, ever do ;). 
    91      
    92     In other cases, sequencing will be best handled by custom algorithms 
    93     within application code; that is, the job of abstracting the sequence 
    94     logic would not be worth the effort. 
    95     """ 
    96      
    97     def __init__(self, type=unicode): 
    98         self.type = type 
    99      
    100     def next(self, sequence): 
    101         raise StopIteration("No sequence defined.") 
    102  
    103  
    104 class UnitSequencerInteger(object): 
    105     """UnitSequencerInteger(type=int, initial=1) 
    106     A sequencer for Unit IDs, where id[i+1] == id[i] + 1.""" 
    107      
    108     def __init__(self, type=int, initial=1): 
    109         self.type = type 
    110         self.initial = initial 
    111      
    112     def next(self, sequence): 
    113         if sequence: 
    114             m = max(sequence) 
    115             if m is not None: 
    116                 return m + 1 
    117         return self.initial 
    118  
    119  
    120 class UnitSequencerUnicode(object): 
    121     """UnitSequencerUnicode(type=unicode, width=6, 
    122         range="abcdefghijklmnopqrstuvwxyz") 
    123     A sequencer for Unit IDs, where e.g. next(['abc']) == 'abd'.""" 
    124      
    125     def __init__(self, type=unicode, width=6, 
    126                  range="abcdefghijklmnopqrstuvwxyz"): 
    127         self.type = type 
    128         self.width = width 
    129         self.range = range 
    130      
    131     def next(self, sequence): 
    132         r = self.range 
    133         if sequence: 
    134             maxid = max(sequence) 
    135             if len(maxid) != self.width: 
    136                 raise OverflowError("'%s' is not of width %s." % 
    137                                     (maxid, self.width)) 
    138             for i in range(self.width - 1, -1, -1): 
    139                 pos = r.index(maxid[i]) + 1 
    140                 if pos >= len(r) or pos < 0: 
    141                     maxid = maxid[:i] + r[0] + maxid[i+1:] 
    142                 else: 
    143                     maxid = maxid[:i] + r[pos] + maxid[i+1:] 
    144                     break 
    145             else: 
    146                 raise OverflowError("Next ID exceeds width %s." % self.width) 
    147             return maxid 
    148         return r[0] * self.width 
    149  
    150  
    151 def _fix_fixedpoint_cmp(): 
    152     """Add methods to fixedpoint to support pickling.""" 
    153     import fixedpoint 
    154     def __cmp__(self, other): 
    155         if other is None: 
    156             return 1 
    157         xn, yn, p = fixedpoint._norm(self, other, FixedPoint=type(self)) 
    158         return cmp(xn, yn) 
    159     fixedpoint.FixedPoint.__cmp__ = __cmp__ 
    160  
    161 def _define_fixedpoint_states(): 
    162     """Add methods to fixedpoint to support pickling.""" 
    163     import fixedpoint 
    164      
    165     if not hasattr(fixedpoint.FixedPoint, "__getstate__"): 
    166         def __getstate__(self): 
    167             return (self.n, self.p) 
    168         fixedpoint.FixedPoint.__getstate__ = __getstate__ 
    169          
    170         def __setstate__(self, v): 
    171             self.n, self.p = v 
    172         fixedpoint.FixedPoint.__setstate__ = __setstate__ 
    173  
    174  
    175 class UnitProperty(object): 
    176     """UnitProperty(type=unicode, index=False, hints=None, key=None) 
    177     Data descriptor for Unit data which will persist in storage. 
    178      
    179     hints: A dictionary which provides named hints to Storage Managers 
    180         concerning the nature of the data. A common use, for example, 
    181         is to inform Managers that would usually store unicode strings 
    182         as strings of length 255, that a particular value should be 
    183         a larger object; this is done with a 'bytes' mapping, such as: 
    184         hints = {u'bytes': 0}, where 0 implies no limit. Canonical storage 
    185         hint names and implementation details may be found in /storage 
    186         documentation. 
    187     """ 
    188      
    189     def __init__(self, type=unicode, index=False, hints=None, key=None): 
    190         if type.__name__ == 'FixedPoint': 
    191             # fixedpoint can't handle "FixedPoint() != None" in Python 2.4 
    192             _fix_fixedpoint_cmp() 
    193              
    194             # fixedpoint.Fixedpoint can't be pickled because it 
    195             # defines __slots__ but not __getstate__. Provide it. 
    196             _define_fixedpoint_states() 
    197          
    198         self.type = type 
    199         self.index = index 
    200         if hints is None: hints = {} 
    201         self.hints = hints 
    202         self.key = key 
    203      
    204     def __get__(self, unit, unitclass=None): 
    205         if unit is None: 
    206             # When calling on the class instead of an instance... 
    207             return self 
    208         else: 
    209             return unit._properties[self.key] 
    210      
    211     def __set__(self, unit, value): 
    212         if self.coerce: 
    213             value = self.coerce(unit, value) 
    214         oldvalue = unit._properties[self.key] 
    215         if oldvalue != value: 
    216             unit._properties[self.key] = 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. 
    276          
    277         Sandboxes receive Units during recall() and memorize(); 
    278         these processes should set the sandbox attribute. 
    279      
    280     dirty: indicates whether elements in the _properties dictionary 
    281         have been modified. This flag is used by Sandboxes to optimize 
    282         forget(): they do not ask Storage Managers to save data for Units 
    283         which have not been modified. Because SM's may cache Units, no code 
    284         should set this flag other than UnitProperty.__set__ and SM's. 
    285     """ 
    286      
    287     __metaclass__ = MetaUnit 
    288     _properties = {} 
    289      
    290     # The default ID type is int. If you wish to use a different type for 
    291     # the ID's of a subclass of Unit, just overwrite ID, e.g.: 
    292     #     ID = UnitProperty(unicode, index=True) 
    293     #       or 
    294     #     UnitSubclass.set_property('ID', unicode, index=True) 
    295     #       or even 
    296     #     UnitSubclass.ID.type = unicode 
    297     # You will probably also want to override Unit.sequencer for the class. 
    298     ID = UnitProperty(int, index=True) 
    299     sequencer = UnitSequencerInteger() 
    300      
    301     def __init__(self, **kwargs): 
    302         # Copy the class _properties dict into self, setting 
    303         # each value to None. 
    304         self._properties = dict.fromkeys(self.__class__._properties.keys()) 
    305          
    306         self.sandbox = None 
    307          
    308         # Make sure we cleanse before assigning properties from kwargs, 
    309         # or the new unit won't get saved if there are no further changes. 
    310         self.cleanse() 
    311         for k, v in kwargs.iteritems(): 
    312             setattr(self, k, v) 
    313      
    314     def _property_hash(self): 
    315         try: 
    316             return sha.new(pickle.dumps(self._properties)).digest() 
    317         except TypeError, x: 
    318             x.args += (self.__class__.__name__, self._properties.keys()) 
    319             raise x 
    320      
    321     def dirty(self): 
    322         return self._initial_property_hash != self._property_hash() 
    323      
    324     def cleanse(self): 
    325         self._initial_property_hash = self._property_hash() 
    326      
    327     def set_property(cls, key, type=unicode, index=False, 
    328                      descriptor=UnitProperty): 
    329         """Set a Unit Property for cls.""" 
    330         setattr(cls, key, descriptor(type, index, key=key)) 
    331         cls._properties[key] = None 
    332     set_property = classmethod(set_property) 
    333      
    334     def set_properties(cls, types={}, descriptor=UnitProperty): 
    335         """Set Unit Properties for cls.""" 
    336         for key, typ in types.items(): 
    337             cls.set_property(key, typ, False, descriptor) 
    338     set_properties = classmethod(set_properties) 
    339      
    340     def indices(cls): 
    341         """cls.indices() -> tuple of names of indexed UnitProperties.""" 
    342         product = [] 
    343         for key in cls.properties(): 
    344             try: 
    345                 if getattr(cls, key).index: 
    346                     product.append(key) 
    347             except AttributeError, x: 
    348                 x.args += (cls, key) 
    349                 raise x 
    350         return tuple(product) 
    351     indices = classmethod(indices) 
    352      
    353     def properties(cls): 
    354         """cls.properties() -> list of UnitProperty names.""" 
    355         return cls._properties.iterkeys() 
    356     properties = classmethod(properties) 
    357      
    358     def property_type(cls, key): 
    359         """cls.property_type(key) -> type of the given UnitProperty.""" 
    360         # Retrieving from the class gives us 
    361         # the UnitProperty object, not its value. 
    362         return getattr(cls, key).type 
    363     property_type = classmethod(property_type) 
    364      
    365     def adjust(self, **values): 
    366         """adjust(**values) -> Set UnitProperties by key, value pairs.""" 
    367         for key, val in values.iteritems(): 
    368             setattr(self, key, val) 
    369      
    370     def repress(self): 
    371         """repress() -> Remove this Unit from memory (do not destroy).""" 
    372         self.sandbox.repress(self) 
    373      
    374     def forget(self): 
    375         """forget() -> Destroy this Unit.""" 
    376         self.sandbox.forget(self) 
    377      
    378     def __copy__(self): 
    379         newUnit = self.__class__() 
    380         for key in self.__class__.properties(): 
    381             if key != u'ID': 
    382                 newUnit._properties[key] = self._properties[key] 
    383         newUnit.ID = None 
    384         newUnit.sandbox = None 
    385         return newUnit 
    386      
    387     def __getstate__(self): 
    388         return (self._properties, self._initial_property_hash) 
    389      
    390     def __setstate__(self, state): 
    391         self.sandbox = None 
    392         self._properties, self._initial_property_hash = state 
    393      
    394      
    395     #                        Associations                        # 
    396      
    397     def first(self, farClass, **kwargs): 
    398         """Return the first associated farClass Unit or None. 
    399          
    400         Passes additional kwargs to sandbox.unit(). 
    401         """ 
    402         try: 
    403             key, farKey = self.__class__._associations[farClass] 
    404         except KeyError: 
    405             raise AssociationError("'%s' is not associated with '%s'" 
    406                                    % (self.__class__, farClass)) 
    407          
    408         value = getattr(self, key) 
    409         if value is None: 
    410             return None 
    411          
    412         # kwargs won't take unicode keys 
    413         kwargs[str(farKey)] = value 
    414         return self.sandbox.unit(farClass, **kwargs) 
    415      
    416     def add(self, *units): 
    417         """add(*units) -> Auto-create a relation between self and unit(s).""" 
    418         cls = self.__class__ 
    419         for unit in units: 
    420             try: 
    421                 key, farKey = cls._associations[unit.__class__] 
    422             except KeyError: 
    423                 raise AssociationError("'%s' is not associated with '%s'" 
    424                                        % (cls, unit.__class__)) 
    425              
    426             nearval = getattr(self, key) 
    427             farval = getattr(unit, farKey) 
    428             if nearval is None: 
    429                 if farval is None: 
    430                     raise AssociationError("At least one Unit key must be set.") 
    431                 else: 
    432                     setattr(self, key, farval) 
    433             else: 
    434                 # If far key is already set, it will simply be overwritten. 
    435                 setattr(unit, farKey, nearval) 
    436  
    437  
    438 def relation_factory(key, farClass, farKey): 
    439     """Produce a new recaller method for a Unit subclass.""" 
    440     def related_units(self, expr=None): 
    441         value = getattr(self, key) 
    442         if value is None: 
    443             return iter([]) 
    444          
    445         # kwargs won't take unicode keys 
    446         f = logic.filter(**{str(farKey): value}) 
    447         if expr is not None: 
    448             f += expr 
    449         return self.sandbox.recall(farClass, f) 
    450      
    451     related_units.__doc__ = ( 
    452     """Iterator over '%(farname)s' Units whose %(farkey)s matches self.%(key)s. 
    453     If self.%(key)s is None, no Units will be recalled.""" 
    454     % {'farname': farClass.__name__, 
    455        'farkey': farKey, 
    456        'key': key, 
    457        }) 
    458     return related_units 
    459  
    460 def associate(cls, key, farClass, farKey, nearFactory=None, farFactory=None): 
    461     """Associate one Unit class with another by relating attributes. 
    462      
    463     cls, key: The 'near' class and its key. 
    464     farClass, farKey: the 'far' class and its key. 
    465      
    466     Far Units will be recalled if their farKey matches cls.key. 
    467     However, if cls.key is empty or None, no Units will be recalled. 
    468     """ 
    469      
    470     # Add a method to cls which retrieves farClass Units 
    471     if nearFactory is None: 
    472         nearFactory = relation_factory 
    473     func = nearFactory(key, farClass, farKey) 
    474     setattr(cls, farClass.__name__, func) 
    475      
    476     # Add the farClass to the association dictionary of cls. 
    477     cls._associations[farClass] = (key, farKey) 
    478      
    479     # Add a method to farClass which retrieves cls Units 
    480     if farFactory is None: 
    481         farFactory = relation_factory 
    482     func = farFactory(farKey, cls, key) 
    483     setattr(farClass, cls.__name__, func) 
    484      
    485     # Add the cls to the association dictionary of farClass. 
    486     farClass._associations[cls] = (farKey, key) 
    487  
    488  
    489 ########################################################################### 
    490 ##                                                                       ## 
    491 ##                                Arenas                                 ## 
    492 ##                                                                       ## 
    493 ########################################################################### 
    494  
    495  
    496 # logging flags (see Arena.logflags) 
    497 LOGSQL = 4 
    498 LOGCONN = 8 
    499  
    500 LOGMEMORIZE = 128 
    501 LOGRECALL = 256 
    502 LOGVIEW = 512 
    503 LOGREPRESS = 1024 
    504 LOGFORGET = 2048 
    505 LOGSANDBOX = LOGMEMORIZE | LOGRECALL | LOGVIEW | LOGREPRESS | LOGFORGET 
    506  
    507  
    508 class Arena(object): 
    509     """Arena(). A namespace/workspace for a Dejavu application.""" 
    510      
    511     def __init__(self): 
    512         self.defaultStore = None 
    513         self.stores = {} 
    514         self._registered_classes = {} 
    515         self.associations = Graph() 
    516         self.engine_functions = {} 
    517         self.logflags = 0 
    518      
    519     def log(self, message, flag): 
    520         """Default logger (writes to stdout). Feel free to replace.""" 
    521         if flag & self.logflags: 
    522             print message 
    523      
    524     def load(self, configFileName): 
    525         """Load StorageManagers.""" 
    526         parser = ConfigParser.ConfigParser() 
    527         # Make names case-sensitive by overriding optionxform. 
    528         parser.optionxform = unicode 
    529         parser.read(configFileName) 
    530          
    531         stores = [] 
    532         for section in parser.sections(): 
    533             opts = dict(parser.items(section)) 
    534             stores.append((int(opts.get("Load Order", "0")), section, opts)) 
    535         stores.sort() 
    536          
    537         for order, name, options in stores: 
    538             self.add_store(name, options[u'Class'], options) 
    539      
    540     def add_store(self, name, store, options=None): 
    541         """add_store(name, store, options=None). Register a StorageManager. 
    542          
    543         The 'store' argument may be the name of a Storage Manager class; 
    544         if so, it must be importable (that is, it must have the full dotted 
    545         package name). 
    546         """ 
    547          
    548         if isinstance(store, basestring): 
    549             import xray 
    550             store = xray.classes(store)(name, self, options or {}) 
    551          
    552         self.stores[name] = store 
    553         if not store.classnames: 
    554             # This store has no "classnames" list, which signals that it 
    555             # handles all classes which are not handled by other stores. 
    556             self.defaultStore = store 
    557         return store 
    558      
    559     def remove_store(self, name): 
    560         if name in self.stores: 
    561             store = self.stores[name] 
    562              
    563             # Disassociate all registered classes with this store. 
    564             for c in self._registered_classes.keys(): 
    565                 if self._registered_classes[c] is store: 
    566                     self._registered_classes[c] = None 
    567              
    568             del self.stores[name] 
    569      
    570     def shutdown(self): 
    571         """Shutdown the arena.""" 
    572         # Tell all stores to shut down. 
    573         stores = [(v.shutdownOrder, v, k) for k, v in self.stores.iteritems()] 
    574         stores.sort() 
    575         for order, store, name in stores: 
    576             store.shutdown() 
    577      
    578     def new_sandbox(self): 
    579         return Sandbox(self) 
    580      
    581     ########################################### 
    582     ##        Unit Class Registration        ## 
    583     ########################################### 
    584      
    585     def register(self, cls): 
    586         """register(cls) -> Assert that Units of class 'cls' will be handled.""" 
    587         # We must allow modules to register classes before any stores have 
    588         # been added, but not overwrite a store which has already been found. 
    589         if cls not in self._registered_classes: 
    590             self._registered_classes[cls] = None 
    591          
    592         # Register any association(s) in an undirected graph. 
    593         for farClass in cls._associations: 
    594             self.associations.connect(cls, farClass) 
    595      
    596     def register_all(self, globals): 
    597         for obj in globals.itervalues(): 
    598             if isinstance(obj, type) and issubclass(obj, Unit): 
    599                 self.register(obj) 
    600      
    601     def class_by_name(self, classname): 
    602         for cls in self._registered_classes: 
    603             if cls.__name__ == classname: 
    604                 return cls 
    605         raise KeyError("No registered class found for '%s'." % classname) 
    606      
    607     def storage(self, cls): 
    608         found = self._registered_classes.get(cls) 
    609          
    610         if found: 
    611             return found 
    612          
    613         # Search all stores for the class name. 
    614         clsname = cls.__name__ 
    615         for store in self.stores.itervalues(): 
    616             if clsname in store.classnames: 
    617                 found = store 
    618                 break 
    619         found = found or self.defaultStore 
    620         if found is None: 
    621             raise KeyError("No store found for '%s' and no " 
    622                            "default store." % clsname) 
    623          
    624         self._registered_classes[cls] = found 
    625         return found 
    626      
    627     def create_storage(self, cls): 
    628         """create_storage(cls). Create storage space for cls.""" 
    629         self.storage(cls).create_storage(cls) 
    630      
    631     def migrate_class(self, cls, new_store): 
    632         """migrate_class(cls, new_store). Copy all units of cls to new_store.""" 
    633         new_store.create_storage(cls) 
    634         for unit in self.new_sandbox().xrecall(cls): 
    635             new_store.reserve(unit) 
    636             new_store.save(unit, True) 
    637      
    638     def migrate(self, new_store, old_store=None, copy_only=False): 
    639         """migrate(new_store, old_store=None). Copy all units (of old_store) to new_store.""" 
    640         for cls in self._registered_classes: 
    641             store = self.storage(cls) 
    642             if old_store is None or old_store is store: 
    643                 self.migrate_class(cls, new_store) 
    644                 if not copy_only: 
    645                     self._registered_classes[cls] = new_store 
    64646 
    64747 
     
    64949# process. Otherwise, you should create your own instance per application. 
    65050dejavuarena = Arena() 
    651  
    652  
    653 ########################################################################### 
    654 ##                                                                       ## 
    655 ##                              Sandboxes                                ## 
    656 ##                                                                       ## 
    657 ########################################################################### 
    658  
    659  
    660 class Sandbox(object): 
    661     """Sandbox(arena). Data sandbox for Dejavu arenas. 
    662      
    663     Each consumer (that is, each UI process) maintains a Sandbox for 
    664     managing Units. Sandboxes populate themselves with Units on a lazy 
    665     basis, allowing UI code to request data as it's needed. However, once 
    666     obtained, such Units are persisted (usually for the lifetime of the 
    667     thread); this important detail means that multiple requests for the 
    668     same Units result in multiple references to the same objects, rather 
    669     than multiple objects. Sandboxes are basically what Fowler calls 
    670     Identity Maps. 
    671      
    672     The *REALLY* important thing to understand if you're customizing this 
    673     is that Sandboxes won't survive sharing across threads--DON'T TRY IT. 
    674     If you need to share unit data across requests, use or make an SM which 
    675     persists the data, and chain it with another, more normal SM. 
    676      
    677     _cache(), _caches, and _stores are private for a reason--don't access 
    678     them from interface code--tell the Sandbox to do it for you. 
    679     """ 
    680      
    681     def __init__(self, arena): 
    682         self.arena = arena 
    683         self._caches = {} 
    684      
    685     def memorize(self, unit): 
    686         """memorize(unit). Persist unit in storage.""" 
    687         cls = unit.__class__ 
    688         unit.sandbox = self 
    689          
    690         # Ask the store to accept the unit, assigning it an ID if 
    691         # necessary. The store should also call unit.cleanse() 
    692         # if it saves the whole unit state on this call. 
    693         self.arena.storage(cls).reserve(unit) 
    694          
    695         # Insert the unit into the cache. 
    696         self._cache(cls)[unit.ID] = unit 
    697         self.arena.log("MEMORIZE %s: %s" % (cls.__name__, unit.ID), LOGMEMORIZE) 
    698          
    699         # Do this at the end of the func, since most on_memorize 
    700         # will want to have an ID when called. 
    701         if hasattr(unit, "on_memorize"): 
    702             unit.on_memorize() 
    703      
    704     def forget(self, unit): 
    705         """Destroy unit, both in the cache and storage.""" 
    706         cls = unit.__class__ 
    707          
    708         self.arena.log("FORGET %s: %s" % (cls.__name__, unit.ID), LOGFORGET) 
    709         self.arena.storage(cls).destroy(unit) 
    710          
    711         del self._cache(cls)[unit.ID] 
    712          
    713         if hasattr(unit, "on_forget"): 
    714             unit.on_forget() 
    715          
    716         unit.sandbox = None 
    717      
    718     def xrecall(self, cls, expr=None): 
    719         """Iterator over units of cls which match expr.""" 
    720          
    721         self.arena.log("RECALL %s: %s" % (cls.__name__, expr), LOGRECALL) 
    722          
    723         cache = self._cache(cls) 
    724          
    725         # Special-case the scenario where one Unit is expected and called 
    726         # by ID. We should be able to save a database hit. 
    727         if expr: 
    728             fc = expr.func.func_code 
    729             if (fc.co_code == '|\x00\x00i\x01\x00d\x01\x00j\x02\x00S' and 
    730                 fc.co_names[-1] == 'ID'): 
    731                 ID = fc.co_consts[-1] 
    732                 unit = cache.get(ID) 
    733                 if unit is not None: 
    734                     # Do NOT call on_recall here. That should be called 
    735                     # only at the Sandbox-SM boundary. 
    736                     yield unit 
    737                     raise StopIteration 
    738          
    739         # Query Cache and Storage. 
    740         for unit in self.arena.storage(cls).recall(cls, expr): 
    741             ID = unit.ID 
    742             # Very important that we check for existing unit, as its 
    743             # state may have changed in memory but not in storage. 
    744             # Make sure the cache lookup and get happens atomically. 
    745             existing = cache.get(ID) 
    746             if existing: 
    747                 yield existing 
    748             else: 
    749                 unit.sandbox = self 
    750                 confirmed = True 
    751                 cache[ID] = unit 
    752                 if hasattr(unit, 'on_recall'): 
    753                     try: 
    754                         unit.on_recall() 
    755                     except UnrecallableError: 
    756                         confirmed = False 
    757                 if confirmed: 
    758                     yield unit 
    759      
    760     def recall(self, cls, expr=None): 
    761         """List of units of class 'cls' which match expr.""" 
    762         return [x for x in self.xrecall(cls, expr)] 
    763      
    764     def multirecall(self, *pairs): 
    765         """multirecall((cls1, expr1), ...) -> [[unit, ...], [unit, ...], ...] 
    766         Recall units of each cls which match each expr. 
    767          
    768         Units of each additional cls/expr pair will be recalled; however, 
    769         only those Units with associations to Units in the PRIMARY set will 
    770         be returned. For you database guys, it's a set of inner joins, 
    771         ALL of which are between the FIRST set and the subsequent set(s). 
    772          
    773         Instead of single Units, each yielded value will be a tuple of 
    774         Units, in the same order as the cls args were supplied. This 
    775         facilitates unpacking in iterative consumer code like: 
    776          
    777         for invoice, price in sandbox.multirecall(Invoice, f, Price, None): 
    778             deal_with(invoice) 
    779             deal_with(price) 
    780         """ 
    781          
    782         self.arena.log("RECALL %s" % ", ".join(["(%s: %s)" % (c.__name__, e) 
    783                                           for c, e in pairs]), 
    784                  LOGRECALL) 
    785         store = self.arena.storage(pairs[0][0]) 
    786         for c, e in pairs: 
    787             if self.arena.storage(c) is not store: 
    788                 raise ValueError(u"multirecall() does not support multiple" 
    789                                  u" classes in disparate stores.") 
    790          
    791         # This is broken. If a filter expr is supplied, then the store may 
    792         # not return rows which our cache would, and those won't be included 
    793         # in the resultset. If you're using multirecall with no expr's, or 
    794         # in read-only scripts, it should be OK for now. But if you mutate 
    795         # Units and then call multirecall, expect inconsistent results. 
    796         for unitset in store.multirecall(*pairs): 
    797             confirmed = True 
    798             for index in xrange(len(unitset)): 
    799                 unit = unitset[index] 
    800                 ID = unit.ID 
    801                 cache = self._cache(unit.__class__) 
    802                 if ID in cache: 
    803                     # Keep the unit which is in our cache! 
    804                     unitset[index] = cache[ID] 
    805                 else: 
    806                     cache[ID] = unit 
    807                     unit.sandbox = self 
    808                     if hasattr(unit, 'on_recall'): 
    809                         try: 
    810                             unit.on_recall() 
    811                         except UnrecallableError: 
    812                             confirmed = False 
    813                             break 
    814             if confirmed: 
    815                 yield unitset 
    816      
    817     def unit(self, cls, **kwargs): 
    818         """unit(cls, **kwargs) -> A single matching Unit, else None. 
    819          
    820         **kwargs will be combined into an Expression via logic.filter. 
    821             The first Unit matching that expression is returned; if no 
    822             Units match, None is returned. 
    823          
    824         If you need a single Unit which matches a more complex 
    825             expression, use recall()[0] or xrecall().next(). 
    826         """ 
    827         expr = None 
    828         if kwargs: 
    829             expr = logic.filter(**kwargs) 
    830         try: 
    831             return self.xrecall(cls, expr).next() 
    832         except StopIteration: 
    833             return None 
    834      
    835     def view(self, cls, attrs, expr=None): 
    836         """view(cls, attrs, expr=None) -> Iterator of all Property tuples.""" 
    837         self.arena.log("VIEW %s [%s]: %s" % (cls.__name__, attrs, expr), LOGVIEW) 
    838          
    839         cache = self._cache(cls) 
    840          
    841         for unit in cache.itervalues(): 
    842             if expr is None or expr.evaluate(unit): 
    843                 yield tuple([getattr(unit, attr) for attr in attrs]) 
    844          
    845         # Add the ID attribute if not present. This is necessary to 
    846         # avoid duplicating objects which are already in our cache. 
    847         fields = list(attrs) 
    848         if "ID" not in fields: 
    849             fields.append("ID") 
    850         index_of_id = fields.index("ID") 
    851          
    852         for row in self.arena.storage(cls).view(cls, fields, expr): 
    853             if row[index_of_id] not in cache: 
    854                 if "ID" not in attrs: 
    855                     # Remove the ID column from the tuple. 
    856                     row = row[:len(row) - 1] 
    857                 yield row 
    858      
    859     def distinct(self, cls, attrs, expr=None): 
    860         """distinct(cls, attrs, expr=None) -> List of distinct Property tuples. 
    861          
    862         If only one attribute is specified, a list of values will be returned. 
    863         If more than one attribute is specified, a zipped list will be returned. 
    864          
    865         Notice that you can also use this function as a count() function 
    866         (in fact it's the only way to do it) by using attrs = ['ID']. 
    867         """ 
    868         self.arena.log("DISTINCT %s [%s]: %s" % (cls.__name__, attrs, expr), LOGVIEW) 
    869          
    870         seen = {} 
    871         cache = self._cache(cls) 
    872         for unit in cache.itervalues(): 
    873             if expr is None or expr.evaluate(unit): 
    874                 row = tuple([getattr(unit, attr) for attr in attrs]) 
    875                 if row not in seen: 
    876                     seen[row] = None 
    877          
    878         for row in self.arena.storage(cls).distinct(cls, attrs, expr): 
    879             if row not in seen: 
    880                 seen[row] = None 
    881          
    882         seen = seen.keys() 
    883         seen.sort() 
    884         if len(attrs) == 1: 
    885             seen = [x[0] for x in seen] 
    886         return seen 
    887      
    888     def count(self, cls, expr): 
    889         """count(cls, expr) -> Number of Units of class 'cls'.""" 
    890         return len(self.distinct(cls, ['ID'], expr)) 
    891      
    892     #################################### 
    893     ##        Cache Management        ## 
    894     #################################### 
    895      
    896     def _cache(self, cls): 
    897         """cache(cls). Return the cache for the specified class. 
    898          
    899         This base class creates a new cache for each cls per request. 
    900         """ 
    901         if cls not in self._caches: 
    902             self._caches[cls] = {} 
    903         return self._caches[cls] 
    904      
    905     def purge(self, cls): 
    906         """purge(cls). Drop all cached Units of class 'cls'. Do not save.""" 
    907         del self._caches[cls] 
    908      
    909     def repress(self, unit): 
    910         """repress(unit). Remove unit from cache (but don't destroy).""" 
    911         cls = unit.__class__ 
    912         self.arena.log("REPRESS %s: %s" % (cls.__name__, unit.ID), LOGREPRESS) 
    913          
    914         if hasattr(unit, "on_repress"): 
    915             unit.on_repress() 
    916          
    917         # Save after on_repress in case on_repress modified the unit. 
    918         self.arena.storage(cls).save(unit) 
    919          
    920         del self._cache(cls)[unit.ID] 
    921      
    922     def flush_all(self): 
    923         """flush_all(). Repress all units.""" 
    924          
    925         for cls in self._caches.keys(): 
    926             # Call all on_repress methods first! There are truly horrible 
    927             # interdependency chains in most on_repress methods, and 
    928             # it's best to resolve them all at once BEFORE flushing 
    929             # any units from the cache. 
    930             # Note we use values instead of itervalues, since the 
    931             # cache may change size during iteration. 
    932             for unit in self._cache(cls).values(): 
    933                 if hasattr(unit, "on_repress"): 
    934                     unit.on_repress() 
    935          
    936         for cls in self._caches.keys(): 
    937             cache = self._cache(cls) 
    938             store = self.arena.storage(cls) 
    939             while cache: 
    940                 unitid, unit = cache.popitem() 
    941                 self.arena.log("REPRESS %s: %s" % (cls.__name__, unitid), LOGREPRESS) 
    942                 store.save(unit) 
    943  
    944  
    945  
    946 ########################################################################### 
    947 ##                                                                       ## 
    948 ##                               Errors                                  ## 
    949 ##                                                                       ## 
    950 ########################################################################### 
    951  
    952  
    953 class DejavuError(Exception): 
    954     """Base class for errors which occur within Dejavu.""" 
    955     def __init__(self, *args): 
    956         Exception.__init__(self) 
    957         self.args = args 
    958      
    959     def __str__(self): 
    960         return u'\n'.join([unicode(eachArg) for eachArg in self.args]) 
    961  
    962 class AssociationError(DejavuError): 
    963     """Exception raised when a Unit association fails.""" 
    964     pass 
    965  
    966 class UnrecallableError(DejavuError): 
    967     """Exception raised when a Unit was sought but not recalled.""" 
    968     pass 
    96951 
    97052 
  • trunk/engines.py

    r100 r101  
    1414    import pickle 
    1515import dejavu 
    16 from dejavu import logic, associate 
     16from dejavu import logic 
    1717import sets 
    1818 
     
    380380            return self.Owner in ('System', 'Public', user) 
    381381 
    382 associate(UnitEngine, 'ID', UnitEngineRule, 'EngineID') 
    383 associate(UnitEngine, 'ID', UnitCollection, 'EngineID') 
     382UnitEngine.one_to_many('ID', UnitEngineRule, 'EngineID') 
     383UnitEngine.one_to_many('ID', UnitCollection, 'EngineID') 
    384384 
    385385 
     
    554554            for eachType in nodes: 
    555555                # Add all associated Units to the collection A. 
    556                 oppfunc = getattr(start, eachType.__name__) 
     556                ua = start._associations[eachType.__name__] 
    557557                cls = self.arena.class_by_name(A.Type) 
    558558                newset = [] 
    559559                if A.universal: 
    560560                    for unit in self.sandbox.recall(cls): 
    561                         for farUnit in oppfunc(unit): 
     561                        farUnits = ua.__get__(unit)() 
     562                        if not ua.to_many: 
     563                            if farUnits is None: 
     564                                farUnits = [] 
     565                            else: 
     566                                farUnits = [farUnits] 
     567                        for farUnit in farUnits: 
    562568                            farid = farUnit.ID 
    563569                            if farid not in newset: 
     
    568574                        unit = self.sandbox.unit(cls, ID=id) 
    569575                        if unit: 
    570                             for farUnit in oppfunc(unit): 
     576                            farUnits = ua.__get__(unit)() 
     577                            if not ua.to_many: 
     578                                if farUnits is None: 
     579                                    farUnits = [] 
     580                                else: 
     581                                    farUnits = [farUnits] 
     582                            for farUnit in farUnits: 
    571583                                farid = farUnit.ID 
    572584                                if farid not in newset: 
  • trunk/storage/db.py

    r100 r101  
    11601160                cls1 = spath.pop(0) 
    11611161                for cls2 in spath: 
    1162                     leftkey, rightkey = cls1._associations[cls2
     1162                    ua = cls1._associations[cls2.__name__
    11631163                    wheres.append("(%s.%s = %s.%s)" % 
    1164                                   (t(cls1), i(leftkey), 
    1165                                    t(cls2), i(rightkey))) 
     1164                                  (t(cls1), i(ua.nearKey), 
     1165                                   t(cls2), i(ua.farKey))) 
    11661166                    cls1 = cls2 
    11671167         
  • trunk/storage/storeshelve.py

    r100 r101  
    169169            for cls2 in spath: 
    170170                subset = [x for x in self.recall(cls2, expr)] 
    171                 leftkey, rightkey = cls1._associations[cls2
    172                 tests.append((subset, leftkey, rightkey)) 
     171                ua = cls1._associations[cls2.__name__
     172                tests.append((subset, ua.nearKey, ua.farKey)) 
    173173                cls1 = cls2 
    174174             
  • trunk/test/test_dejavu.py

    r100 r101  
    1414        # CleanUP The Database! 
    1515        box = zoo_fixture.arena.new_sandbox() 
    16         for animal in box.recall( zoo_fixture.Animal ): 
     16        for animal in box.recall(zoo_fixture.Animal): 
    1717            animal.forget() 
    18         for zoo_thing in box.recall( zoo_fixture.Zoo ): 
     18        for zoo_thing in box.recall(zoo_fixture.Zoo): 
    1919            zoo_thing.forget() 
    2020     
     
    107107         
    108108        # Retrieve the Unit from the same sandbox again. 
    109         self.assert_(box.recall(zoo_fixture.Animal)[0] is bat) 
     109        self.assert_(box.unit(zoo_fixture.Animal) is bat) 
    110110         
    111111        # Retrieve the Unit from a new sandbox. 
    112112        # Units should be different, and their 
    113113        # UnitProperties should be different. 
    114         bat3 = zoo_fixture.arena.new_sandbox().recall(zoo_fixture.Animal)[0] 
     114        bat3 = zoo_fixture.arena.new_sandbox().unit(zoo_fixture.Animal) 
    115115        self.assert_(bat3 is not bat) 
    116116        self.assertEqual(bat3.Legs, 4) 
  • trunk/test/zoo_fixture.py

    r100 r101  
    1717import dejavu 
    1818from dejavu import logic 
    19 from dejavu import Unit, UnitProperty, associate 
     19from dejavu import Unit, UnitProperty, ToOne, ToMany 
    2020from dejavu.test import tools 
     21 
     22 
     23class EscapeProperty(UnitProperty): 
     24    def __set__(self, unit, value): 
     25        UnitProperty.__set__(self, unit, value) 
     26        # Zoo is a ToOne association, so it will return a unit or None. 
     27        z = unit.Zoo() 
     28        if z: 
     29            z.LastEscape = unit.LastEscape 
     30 
     31 
     32class Animal(Unit): 
     33    Species = UnitProperty() 
     34    ZooID = UnitProperty(int, index=True) 
     35    Legs = UnitProperty(int) 
     36    PreviousZoos = UnitProperty(list) 
     37    LastEscape = EscapeProperty(datetime.datetime) 
     38    Lifespan = UnitProperty(float, hints={'bytes': 4}) 
     39    MotherID = UnitProperty(int) 
     40##    Mother = ToMany('MotherID', 'ID', Animal) 
    2141 
    2242 
     
    3252        Admission = UnitProperty(float) 
    3353 
    34  
    35 class EscapeProperty(UnitProperty): 
    36     def __set__(self, unit, value): 
    37         UnitProperty.__set__(self, unit, value) 
    38         z = unit.first(Zoo) 
    39         if z: 
    40             z.LastEscape = unit.LastEscape 
    41  
    42  
    43 class Animal(Unit): 
     54Zoo.one_to_many('ID', Animal, 'ZooID') 
     55 
     56 
     57class Vet(Unit): 
     58    """A Veterinarian.""" 
    4459    Name = UnitProperty() 
    4560    ZooID = UnitProperty(int, index=True) 
    46     Legs = UnitProperty(int) 
    47     PreviousZoos = UnitProperty(list) 
    48     LastEscape = EscapeProperty(datetime.datetime) 
    49     Lifespan = UnitProperty(float, hints={'bytes': 4}) 
    50     Mother = UnitProperty(int) 
    51      
    52     def in_first_zoo(self): 
    53         return (self.PreviousZoo and self.PreviousZoo[0] == self.ZooID) 
    54 associate(Zoo, 'ID', Animal, 'ZooID') 
    55 associate(Animal, 'ID', Animal, 'Mother') 
     61 
     62Vet.many_to_one('ZooID', Zoo, 'ID') 
     63 
     64 
     65class Visit(Unit): 
     66    """Work done by a Veterinarian on an Animal.""" 
     67    VetID = UnitProperty(int, index=True) 
     68    AnimalID = UnitProperty(int, index=True) 
     69    Date = UnitProperty(datetime.date) 
     70 
     71Vet.one_to_many('ID', Visit, 'VetID') 
     72Animal.one_to_many('ID', Visit, 'AnimalID') 
    5673 
    5774 
     
    6683    else: 
    6784        Acreage = UnitProperty(float) 
    68 associate(Zoo, 'ID', Exhibit, 'ZooID') 
     85 
     86Zoo.one_to_many('ID', Exhibit, 'ZooID') 
     87 
     88 
     89Jan_1_2001 = datetime.date(2001, 1, 1) 
     90every13days = [Jan_1_2001 + datetime.timedelta(x * 13) for x in range(20)] 
     91every17days = [Jan_1_2001 + datetime.timedelta(x * 17) for x in range(20)] 
    6992 
    7093 
    7194class ZooTests(unittest.TestCase): 
    7295     
    73     def test_0_populate(self): 
     96    def test_1_schema(self): 
     97        self.assertEqual(Zoo.Animal.__class__, dejavu.ToMany) 
     98        self.assertEqual(Zoo.Animal.nearClass, Zoo) 
     99        self.assertEqual(Zoo.Animal.nearKey, 'ID') 
     100        self.assertEqual(Zoo.Animal.farClass, Animal) 
     101        self.assertEqual(Zoo.Animal.farKey, 'ZooID') 
     102         
     103        self.assertEqual(Animal.Zoo.__class__, dejavu.ToOne) 
     104        self.assertEqual(Animal.Zoo.nearClass, Animal) 
     105        self.assertEqual(Animal.Zoo.nearKey, 'ZooID') 
     106        self.assertEqual(Animal.Zoo.farClass, Zoo) 
     107        self.assertEqual(Animal.Zoo.farKey, 'ID') 
     108     
     109    def test_2_populate(self): 
    74110        box = arena.new_sandbox() 
    75111         
     
    77113        # dirtied via __init__ is still saved. 
    78114        WAP = Zoo(Name = 'Wild Animal Park', 
    79                       Founded = datetime.date(2000, 1, 1), 
    80                       # 59 can give rounding errors with divmod, which 
    81                       # AdapterFromADO needs to correct. 
    82                       Opens = datetime.time(8, 15, 59), 
    83                       LastEscape = datetime.datetime(2004, 7, 29, 5, 6, 7), 
    84                       Admission = "4.95", 
     115                  Founded = datetime.date(2000, 1, 1), 
     116                  # 59 can give rounding errors with divmod, which 
     117                  # AdapterFromADO needs to correct. 
     118                  Opens = datetime.time(8, 15, 59), 
     119                  LastEscape = datetime.datetime(2004, 7, 29, 5, 6, 7), 
     120                  Admission = "4.95", 
     121                  ) 
     122        box.memorize(WAP) 
     123         
     124        SDZ = Zoo(Name = 'San Diego Zoo', 
     125                  # This early date should play havoc with a number 
     126                  # of implementations. 
     127                  Founded = datetime.date(1835, 9, 13), 
     128                  Opens = datetime.time(9, 0, 0), 
     129                  Admission = "0", 
     130                  ) 
     131        box.memorize(SDZ) 
     132         
     133        Biodome = Zoo(Name = u'Montr\xe9al Biod\xf4me', 
     134                      Founded = datetime.date(1992, 6, 19), 
     135                      Opens = datetime.time(9, 0, 0), 
     136                      Admission = "11.75", 
    85137                      ) 
    86         box.memorize(WAP) 
    87          
    88         SDZ = Zoo(Name = 'San Diego Zoo', 
    89                       # This early date should play havoc with a number 
    90                       # of implementations. 
    91                       Founded = datetime.date(1835, 9, 13), 
    92                       Opens = datetime.time(9, 0, 0), 
    93                       Admission = "0", 
    94                       ) 
    95         box.memorize(SDZ) 
    96          
    97         Biodome = Zoo(Name = u'Montr\xe9al Biod\xf4me', 
    98                           Founded = datetime.date(1992, 6, 19), 
    99                           Opens = datetime.time(9, 0, 0), 
    100                           Admission = "11.75", 
    101                           ) 
    102138        box.memorize(Biodome) 
    103139         
    104         seaworld = Zoo(Name = 'Sea_World', 
    105                            Admission = "60", 
    106                            ) 
     140        seaworld = Zoo(Name = 'Sea_World', Admission = "60") 
    107141        box.memorize(seaworld) 
    108142         
    109143        # Animals 
    110         leopard = Animal(Name='Leopard', Legs=4, Lifespan=73.5) 
     144        leopard = Animal(Species='Leopard', Legs=4, Lifespan=73.5) 
    111145        self.assertEqual(leopard.PreviousZoos, None) 
    112146        box.memorize(leopard) 
     
    114148        leopard.LastEscape = datetime.datetime(2004, 12, 21, 8, 15, 0) 
    115149         
    116         box.memorize(Animal(Name='Slug', Legs=1, Lifespan=.75)) 
    117         tiger = Animal(Name='Tiger', Legs=4) 
     150        box.memorize(Animal(Species='Slug', Legs=1, Lifespan=.75)) 
     151        tiger = Animal(Species='Tiger', Legs=4) 
    118152        box.memorize(tiger) 
    119         box.memorize(Animal(Name='Lion', Legs=4)) 
    120         box.memorize(Animal(Name='Bear', Legs=4)) 
     153        box.memorize(Animal(Species='Lion', Legs=4)) 
     154        box.memorize(Animal(Species='Bear', Legs=4)) 
    121155        # Notice that ostrich.PreviousZoos is [], whereas leopard is None. 
    122         box.memorize(Animal(Name='Ostrich', Legs=2, PreviousZoos=[], 
    123                                 Lifespan=103.2)) 
    124         box.memorize(Animal(Name='Centipede', Legs=100)) 
    125          
    126         emp = Animal(Name='Emperor Penguin', Legs=2) 
     156        box.memorize(Animal(Species='Ostrich', Legs=2, PreviousZoos=[], 
     157                            Lifespan=103.2)) 
     158        box.memorize(Animal(Species='Centipede', Legs=100)) 
     159         
     160        emp = Animal(Species='Emperor Penguin', Legs=2) 
    127161        box.memorize(emp) 
    128         adelie = Animal(Name='Adelie Penguin', Legs=2) 
     162        adelie = Animal(Species='Adelie Penguin', Legs=2) 
    129163        box.memorize(adelie) 
    130164         
    131165        seaworld.add(emp, adelie) 
    132166         
    133         millipede = Animal(Name='Millipede', Legs=1000000) 
     167        millipede = Animal(Species='Millipede', Legs=1000000) 
    134168        millipede.PreviousZoos = [WAP.ID] 
    135169        box.memorize(millipede) 
     
    138172         
    139173        # Add a mother and child to test relationships 
    140         bai_yun = Animal(Name='Ape', Legs=2) 
    141         box.memorize(bai_yun) 
    142         hua_mei = Animal(Name='Ape', Legs=2, Mother=bai_yun.ID) 
    143         box.memorize(hua_mei) 
     174        bai_yun = Animal(Species='Ape', Legs=2) 
     175        box.memorize(bai_yun)   # ID = 11 
     176        hua_mei = Animal(Species='Ape', Legs=2, MotherID=bai_yun.ID) 
     177        box.memorize(hua_mei)   # ID = 12 
    144178         
    145179        # Exhibits 
    146180        pe = Exhibit(Name = 'The Penguin Encounter', 
    147                          ZooID = seaworld.ID, 
    148                          Animals = [emp.ID, adelie.ID], 
    149                          PettingAllowed = True, 
    150                          Acreage = "3.21", 
    151                         
     181                     ZooID = seaworld.ID, 
     182                     Animals = [emp.ID, adelie.ID], 
     183                     PettingAllowed = True, 
     184                     Acreage = "3.21", 
     185                     
    152186        box.memorize(pe) 
    153187         
    154188        tr = Exhibit(Name = 'Tiger River', 
    155                          ZooID = SDZ.ID, 
    156                          Animals = [tiger.ID], 
    157                          PettingAllowed = False, 
    158                          Acreage = "4", 
    159                         
     189                     ZooID = SDZ.ID, 
     190                     Animals = [tiger.ID], 
     191                     PettingAllowed = False, 
     192                     Acreage = "4", 
     193                     
    160194        box.memorize(tr) 
    161195         
    162         box.flush_all() 
    163      
    164     def test_1_Object_Properties(self): 
     196        # Vets 
     197        cs = Vet(Name = 'Charles Schroeder', ZooID = SDZ.ID) 
     198        box.memorize(cs) 
     199        jm = Vet(Name = 'Jim McBain', ZooID = seaworld.ID) 
     200        box.memorize(jm) 
     201         
     202        # Visits 
     203        for d in every13days: 
     204            box.memorize(Visit(VetID=cs.ID, AnimalID=tiger.ID, Date=d)) 
     205        for d in every17days: 
     206            box.memorize(Visit(VetID=jm.ID, AnimalID=emp.ID, Date=d)) 
     207         
     208        box.flush_all() 
     209     
     210    def test_3_Object_Properties(self): 
    165211        box = arena.new_sandbox() 
    166212         
     
    198244         
    199245        # Animals 
    200         leopard = box.unit(Animal, Name='Leopard') 
    201         self.assertEqual(leopard.Name, 'Leopard') 
     246        leopard = box.unit(Animal, Species='Leopard') 
     247        self.assertEqual(leopard.Species, 'Leopard') 
    202248        self.assertEqual(leopard.Legs, 4) 
    203249        self.assertEqual(leopard.Lifespan, 73.5) 
     
    207253                         datetime.datetime(2004, 12, 21, 8, 15, 0)) 
    208254         
    209         ostrich = box.unit(Animal, Name='Ostrich') 
    210         self.assertEqual(ostrich.Name, 'Ostrich') 
     255        ostrich = box.unit(Animal, Species='Ostrich') 
     256        self.assertEqual(ostrich.Species, 'Ostrich') 
    211257        self.assertEqual(ostrich.Legs, 2) 
    212258        self.assertEqual(ostrich.ZooID, None) 
     
    215261         
    216262        millipede = box.unit(Animal, Legs=1000000) 
    217         self.assertEqual(millipede.Name, 'Millipede') 
     263        self.assertEqual(millipede.Species, 'Millipede') 
    218264        self.assertEqual(millipede.Legs, 1000000) 
    219265        self.assertEqual(millipede.ZooID, SDZ.ID) 
     
    242288        box.flush_all() 
    243289     
    244     def test_2_Expressions(self): 
     290    def test_4_Expressions(self): 
    245291        box = arena.new_sandbox() 
    246292         
     
    259305        self.assertEqual(matches(lambda x: x.Legs > 10), 2) 
    260306        self.assertEqual(matches(lambda x: x.Lifespan > 70), 2) 
    261         self.assertEqual(matches(lambda x: x.Name.startswith('L')), 2) 
    262         self.assertEqual(matches(lambda x: x.Name.endswith('pede')), 2) 
     307        self.assertEqual(matches(lambda x: x.Species.startswith('L')), 2) 
     308        self.assertEqual(matches(lambda x: x.Species.endswith('pede')), 2) 
    263309        self.assertEqual(matches(lambda x: x.LastEscape != None), 1) 
    264310        self.assertEqual(matches(lambda x: None == x.LastEscape), 11) 
    265311         
    266312        # In operator (containedby) 
    267         self.assertEqual(matches(lambda x: 'pede' in x.Name), 2) 
    268         self.assertEqual(matches(lambda x: x.Name in ('Lion', 'Tiger', 'Bear')), 3) 
     313        self.assertEqual(matches(lambda x: 'pede' in x.Species), 2) 
     314        self.assertEqual(matches(lambda x: x.Species in ('Lion', 'Tiger', 'Bear')), 3) 
    269315         
    270316        # Try In with cell references 
     
    272318        pet, pet2 = thing(), thing() 
    273319        pet.Name, pet2.Name = 'Slug', 'Ostrich' 
    274         self.assertEqual(matches(lambda x: x.Name in (pet.Name, pet2.Name)), 2) 
     320        self.assertEqual(matches(lambda x: x.Species in (pet.Name, pet2.Name)), 2) 
    275321         
    276322        # logic and other functions 
    277         self.assertEqual(matches(lambda x: dejavu.ieq(x.Name, 'slug')), 1) 
    278         self.assertEqual(matches(lambda x: dejavu.icontains(x.Name, 'PEDE')), 2) 
    279         self.assertEqual(matches(lambda x: dejavu.icontains(('Lion', 'Banana'), x.Name)), 1) 
    280         f = lambda x: dejavu.icontainedby(x.Name, ('Lion', 'Bear', 'Leopard')) 
     323        self.assertEqual(matches(lambda x: dejavu.ieq(x.Species, 'slug')), 1) 
     324        self.assertEqual(matches(lambda x: dejavu.icontains(x.Species, 'PEDE')), 2) 
     325        self.assertEqual(matches(lambda x: dejavu.icontains(('Lion', 'Banana'), x.Species)), 1) 
     326        f = lambda x: dejavu.icontainedby(x.Species, ('Lion', 'Bear', 'Leopard')) 
    281327        self.assertEqual(matches(f), 3) 
    282328        name = 'Lion' 
    283         self.assertEqual(matches(lambda x: len(x.Name) == len(name)), 3) 
     329        self.assertEqual(matches(lambda x: len(x.Species) == len(name)), 3) 
    284330         
    285331        # This broke sometime in 2004. Rev 32 seems to have fixed it. 
    286         self.assertEqual(matches(lambda x: 'i' in x.Name), 7) 
     332        self.assertEqual(matches(lambda x: 'i' in x.Species), 7) 
    287333         
    288334        # Test now(), today(), year() 
     
    295341        # Notice that we reference a method ('count') which no 
    296342        # known SM handles, so it will default back to Expr.eval(). 
    297         self.assertEqual(matches(lambda x: 'p' in x.Name 
    298                                  and x.Name.count('e') > 1), 3) 
     343        self.assertEqual(matches(lambda x: 'p' in x.Species 
     344                                 and x.Species.count('e') > 1), 3) 
    299345         
    300346        # This broke in MSAccess (storeado) in April 2005, due to a bug in 
     
    316362        self.assertEqual(len(units), 1) 
    317363     
    318     def test_3_Aggregates(self): 
     364    def test_5_Aggregates(self): 
    319365        box = arena.new_sandbox() 
    320366         
     
    336382                    'Ape': None, 
    337383                    } 
    338         for name, lifespan in box.view(Animal, ['Name', 'Lifespan']): 
    339             if expected[name] is None: 
     384        for species, lifespan in box.view(Animal, ['Species', 'Lifespan']): 
     385            if expected[species] is None: 
    340386                self.assertEqual(lifespan, None) 
    341387            else: 
    342                 self.assertAlmostEqual(expected[name], lifespan, places=5) 
     388                self.assertAlmostEqual(expected[species], lifespan, places=5) 
    343389         
    344390        # distinct 
     
    348394         
    349395        # This may raise a warning on some DB's. 
    350         f = logic.Expression(lambda x: x.Name == 'Lion') 
     396        f = logic.Expression(lambda x: x.Species == 'Lion') 
    351397        escapees = box.distinct(Animal, ['Legs'], f) 
    352398        self.assertEqual(escapees, [4]) 
    353399     
    354     def test_4_Multiselect(self): 
    355         box = arena.new_sandbox() 
     400    def test_6_Multiselect(self): 
     401        box = arena.new_sandbox() 
     402        f = logic.filter(Name='San Diego Zoo') 
    356403        zooed_animals = [(z, a) for z, a in 
    357                          box.multirecall((Zoo, logic.filter(Name='San Diego Zoo')), 
    358                                          (Animal, None))] 
     404                         box.multirecall((Zoo, f), (Animal, None))] 
    359405        SDZ = box.unit(Zoo, Name='San Diego Zoo') 
    360406        self.assertEqual(len(zooed_animals), 2) 
     
    367413        # Assert that multirecalls with no matching secondary units returns 
    368414        # no matches for the initial class. 
     415        leo = logic.filter(Species='Leopard') 
    369416        zooed_animals = [(z, a) for z, a in 
    370                          box.multirecall((Zoo, logic.filter(Name='San Diego Zoo')), 
    371                                          (Animal, logic.filter(Name='Leopard')))] 
     417                         box.multirecall((Zoo, f), (Animal, leo))] 
    372418        self.assertEqual(len(zooed_animals), 0) 
    373419     
    374     def test_5_Multithreading(self): 
     420    def test_7_Multithreading(self): 
    375421        f = logic.Expression(lambda x: x.Legs == 4) 
    376422        def thread_recall(): 
     
    390436            t.join() 
    391437     
    392     def test_6_Editing(self): 
     438    def test_8_Editing(self): 
    393439        # Edit 
    394440        box = arena.new_sandbox() 
     
    432478            self.assertEqual(SDZ.Admission, 0.0) 
    433479     
    434     def test_7_Iteration(self): 
     480    def test_9_Iteration(self): 
    435481        box = arena.new_sandbox() 
    436482         
    437483        # Test box.unit inside of xrecall 
    438         for ape in box.xrecall(Animal, logic.filter(Name='Ape')): 
    439             mother = box.unit(Animal, ID=ape.Mother) 
    440             if ape.ID == 11: 
    441                 self.assertEqual(mother, None) 
    442             else: 
    443                 self.assertEqual(mother.ID, 11) 
     484        for visit in box.xrecall(Visit, logic.filter(VetID=1)): 
     485            firstvisit = box.unit(Visit, VetID=1, Date=Jan_1_2001) 
     486            self.assertEqual(firstvisit.VetID, 1) 
     487            self.assertEqual(visit.VetID, 1) 
    444488         
    445489        # Test recall inside of xrecall 
    446         for ape in box.xrecall(Animal, logic.filter(Name='Ape')): 
    447             children = 0 
    448             for child in box.recall(Animal, logic.filter(Mother=ape.ID)): 
    449                 children += 1 
    450             if ape.ID == 11: 
    451                 self.assertEqual(children, 1) 
    452             else: 
    453                 self.assertEqual(children, 0) 
     490        for visit in box.xrecall(Visit, logic.filter(VetID=1)): 
     491            f = logic.Expression(lambda x: x.VetID == 1 and x.ID != visit.ID) 
     492            othervisits = box.recall(Visit, f) 
     493            self.assertEqual(len(othervisits), len(every13days) - 1) 
    454494         
    455495        # Test far associations inside of xrecall 
    456         for ape in box.xrecall(Animal, logic.filter(Name='Ape')): 
    457             mother = ape.first(Animal) 
    458             if ape.ID == 11: 
    459                 self.assertEqual(mother, None) 
    460             else: 
    461                 self.assertEqual(mother.ID, 11) 
     496        for visit in box.xrecall(Visit, logic.filter(VetID=1)): 
     497            # visit.Vet is a ToOne association, so will return a unit or None. 
     498            vet = visit.Vet() 
     499            self.assertEqual(vet.ID, 1) 
    462500 
    463501 
     
    474512    arena.register_all(globals()) 
    475513     
    476     for cls in (Animal, Zoo, Exhibit): 
     514    for cls in (Animal, Zoo, Exhibit, Vet, Visit): 
    477515        arena.create_storage(cls) 
    478516