Changeset 175

Show
Ignore:
Timestamp:
08/06/07 14:16:45 (7 years ago)
Author:
ged
Message:

merged autodelay branch!

Location:
elixir/trunk
Files:
13 modified

Legend:

Unmodified
Added
Removed
  • elixir/trunk/elixir/__init__.py

    r148 r175  
    2929                                 has_and_belongs_to_many 
    3030from elixir.properties import has_property 
     31from elixir.statements import Statement 
    3132 
    3233try: 
     
    4243           'using_options', 'using_table_options', 'using_mapper_options', 
    4344           'options_defaults', 'metadata', 'objectstore', 
    44            'create_all', 'drop_all', 'setup_all', 'cleanup_all', 
    45            'delay_setup'] + \ 
     45           'create_all', 'drop_all', 'setup_all', 'cleanup_all'] + \ 
    4646          sqlalchemy.types.__all__ 
    4747 
    4848__pudge_all__ = ['create_all', 'drop_all', 'setup_all', 'cleanup_all', 
    49                  'metadata', 'objectstore', 'delay_setup'] 
     49                 'metadata', 'objectstore'] 
    5050 
    5151# connect 
    52 metadata = sqlalchemy.DynamicMetaData(threadlocal=False) 
     52metadata = sqlalchemy.MetaData() 
    5353 
    5454try: 
     
    8383        md.drop_all() 
    8484 
    85 delayed_entities = set() 
    86 delay_setup = False 
     85_delayed_descriptors = list() 
     86 
     87def setup_all(create_tables=False): 
     88    '''Setup the table and mapper for all entities which have been delayed. 
     89 
     90    This is called automatically when your entity is first accessed, or ... 
     91    [TODO: complete this] 
     92    ''' 
     93 
     94    if not _delayed_descriptors: 
     95        return 
     96 
     97#TODO: define all those operations as methods on the descriptor 
     98#    for method_name in ('setup_table', 'setup_mapper', 'setup_relkeys', ...): 
     99#        for desc in _delayed_descriptors: 
     100#            method = getattr(desc, method_name) 
     101#            method() 
     102    try: 
     103        for desc in _delayed_descriptors: 
     104            entity = desc.entity 
     105            entity._ready = False 
     106            del sqlalchemy.orm.mapper_registry[entity._class_key] 
     107 
     108            md = desc.metadata 
     109            # the table could have already been removed (namely in a single  
     110            # table inheritance scenario) 
     111            md.tables.pop(entity._table_key, None) 
     112 
     113            # restore original table iterator if not done already 
     114            if hasattr(md.table_iterator, '_non_elixir_patched_iterator'): 
     115                md.table_iterator = \ 
     116                    md.table_iterator._non_elixir_patched_iterator 
     117 
     118        # Make sure autoloaded tables are available so that we can setup  
     119        # foreign keys to their columns 
     120        for desc in _delayed_descriptors: 
     121            if desc.autoload: 
     122                desc.setup_table() 
     123 
     124        for desc in _delayed_descriptors: 
     125            desc.create_pk_cols() 
     126 
     127        # Create other columns from belongs_to relationships. 
     128        for desc in _delayed_descriptors: 
     129            for rel in desc.relationships.itervalues(): 
     130                rel.create_keys(False) 
     131 
     132        for desc in _delayed_descriptors: 
     133            if not desc.autoload: 
     134                desc.setup_table() 
     135 
     136        for desc in _delayed_descriptors: 
     137            for rel in desc.relationships.itervalues(): 
     138                rel.create_tables() 
     139 
     140        for desc in _delayed_descriptors: 
     141            desc.setup_events() 
     142         
     143        for desc in _delayed_descriptors: 
     144            desc.setup_mapper() 
     145 
     146        for desc in _delayed_descriptors: 
     147            for rel in desc.relationships.itervalues(): 
     148                rel.create_properties() 
     149 
     150        #TODO: merge this with the "when" feature of statements 
     151        for desc in _delayed_descriptors: 
     152            # allow the statements to do any "finalization" 
     153            Statement.finalize(desc.entity) 
     154 
     155    finally: 
     156        # make sure that even if we fail to initialize, we don't leave junk for 
     157        # others 
     158        del _delayed_descriptors[:] 
     159 
     160    # issue the "CREATE" SQL statements 
     161    if create_tables: 
     162        create_all() 
    87163 
    88164 
    89 def setup_all(): 
    90     '''Setup the table and mapper for all entities which have been delayed. 
    91  
    92     This should be used in conjunction with setting ``delay_setup`` to ``True`` 
    93     before defining your entities. 
    94     ''' 
    95     for entity in delayed_entities: 
    96         entity.setup_table() 
    97     for entity in delayed_entities: 
    98         entity.setup_mapper() 
    99  
    100     # setup all relationships 
    101     for entity in delayed_entities: 
    102         for rel in entity.relationships.itervalues(): 
    103             rel.setup() 
    104  
    105     delayed_entities.clear() 
    106  
    107     # issue the "CREATE" SQL statements 
    108     create_all() 
    109  
    110  
    111 def cleanup_all(): 
     165def cleanup_all(drop_tables=False): 
    112166    '''Drop table and clear mapper for all entities, and clear all metadatas. 
    113167    ''' 
    114     drop_all() 
     168    if drop_tables: 
     169        drop_all() 
     170         
    115171    for md in metadatas: 
    116172        md.clear() 
    117173    metadatas.clear() 
    118     EntityDescriptor.uninitialized_rels.clear() 
    119174 
    120175    objectstore.clear() 
    121     sqlalchemy.clear_mappers() 
     176    sqlalchemy.orm.clear_mappers() 
    122177 
  • elixir/trunk/elixir/entity.py

    r170 r175  
    33''' 
    44 
    5 from sqlalchemy                     import Table, Integer, desc 
     5from sqlalchemy                     import Table, Integer, String, desc,\ 
     6                                           ForeignKey 
    67from sqlalchemy.orm                 import deferred, Query, MapperExtension 
    78from sqlalchemy.ext.assignmapper    import assign_mapper 
    89from sqlalchemy.util                import OrderedDict 
     10import sqlalchemy 
    911from elixir.statements              import Statement 
    1012from elixir.fields                  import Field 
     
    1719 
    1820import sys 
     21import warnings 
     22 
    1923import elixir 
    2024import inspect 
     
    2529DEFAULT_AUTO_PRIMARYKEY_TYPE = Integer 
    2630DEFAULT_VERSION_ID_COL = "row_version" 
     31DEFAULT_POLYMORPHIC_COL_NAME = "row_type" 
     32DEFAULT_POLYMORPHIC_COL_SIZE = 20 
     33DEFAULT_POLYMORPHIC_COL_TYPE = String(DEFAULT_POLYMORPHIC_COL_SIZE) 
    2734 
    2835class EntityDescriptor(object): 
     
    4451 
    4552        self.parent = None 
     53        self.children = [] 
     54 
    4655        for base in entity.__bases__: 
    4756            if issubclass(base, Entity) and base is not Entity: 
     
    5261                else: 
    5362                    self.parent = base 
     63                    self.parent._descriptor.children.append(entity) 
    5464 
    5565        self.fields = OrderedDict() 
     66        #TODO Ordered 
    5667        self.relationships = dict() 
    5768        self.delayed_properties = dict() 
    5869        self.constraints = list() 
    59  
    60         #CHECKME: this is a workaround for the "current" descriptor/target 
    61         # property ugliness. The problem is that this workaround is ugly too. 
    62         # I'm not sure if this is a safe practice. It works but...? 
    63 #        setattr(self.module, entity.__name__, entity) 
    6470 
    6571        # set default value for options 
     
    6874        self.metadata = getattr(self.module, 'metadata', elixir.metadata) 
    6975 
    70         for option in ('inheritance',  
     76        for option in ('inheritance', 'polymorphic', 
    7177                       'autoload', 'tablename', 'shortnames',  
    7278                       'auto_primarykey', 
     
    8591 
    8692        entity = self.entity 
     93        if self.inheritance == 'concrete' and self.polymorphic: 
     94            raise NotImplementedError("Polymorphic concrete inheritance is " 
     95                                      "not yet implemented.") 
     96 
     97        if self.parent: 
     98            if self.inheritance == 'single': 
     99                self.tablename = self.parent._descriptor.tablename 
    87100 
    88101        if not self.tablename: 
     
    96109            self.tablename = self.tablename(entity) 
    97110     
    98     def setup(self): 
    99         ''' 
    100         Create tables, keys, columns that have been specified so far and  
    101         assign a mapper. Will be called when an instance of the entity is  
    102         created or a mapper is needed to access one or many instances of the  
    103         entity. It will try to initialize the entity's relationships (along  
    104         with any delayed relationship) but some of them might be delayed. 
    105         ''' 
    106         if elixir.delay_setup: 
    107             elixir.delayed_entities.add(self) 
    108             return 
    109          
    110         self.setup_events() 
    111         self.setup_table() 
    112         self.setup_mapper() 
    113  
    114         # This marks all relations of the entity (or, at least those which  
    115         # have been added so far by statements) as being uninitialized 
    116         EntityDescriptor.uninitialized_rels.update( 
    117             self.relationships.values()) 
    118  
    119         # try to setup all uninitialized relationships 
    120         EntityDescriptor.setup_relationships() 
    121          
    122         # finally, allow the statement to do any "finalization" 
    123         Statement.finalize(self.entity) 
    124      
    125111    def setup_events(self): 
    126112        # create a list of callbacks for each event 
     
    179165            kwargs['version_id_col'] = self.fields[self.version_id_col].column 
    180166 
    181         if self.parent: 
    182             if self.inheritance == 'single': 
    183                 # at this point, we don't know whether the parent relationships 
    184                 # have already been processed or not. Some of them might be,  
    185                 # some other might not. 
    186                 if not self.parent.mapper: 
    187                     self.parent._descriptor.setup_mapper() 
     167        if self.inheritance in ('single', 'concrete', 'multi'): 
     168            if self.parent and \ 
     169               not (self.inheritance == 'concrete' and not self.polymorphic): 
    188170                kwargs['inherits'] = self.parent.mapper 
     171 
     172            if self.polymorphic: 
     173                if self.children and not self.parent: 
     174                    kwargs['polymorphic_on'] = \ 
     175                        self.fields[self.polymorphic].column 
     176                    if self.inheritance == 'multi': 
     177                        children = self._get_children() 
     178                        join = self.entity.table 
     179                        for child in children: 
     180                            join = join.outerjoin(child.table) 
     181                        kwargs['select_table'] = join 
     182                     
     183                if self.children or self.parent: 
     184                    #TODO: make this customizable (both callable and string) 
     185                    #TODO: include module name 
     186                    kwargs['polymorphic_identity'] = \ 
     187                        self.entity.__name__.lower() 
     188 
     189                if self.inheritance == 'concrete': 
     190                    kwargs['concrete'] = True 
    189191 
    190192        properties = dict() 
     
    197199                                                         group=group) 
    198200 
    199         #TODO: make this happen after the rel columns have been added. 
    200201        for name, prop in self.delayed_properties.iteritems(): 
    201202            properties[name] = self.evaluate_property(prop) 
     
    207208                colname in kwargs['primary_key']] 
    208209 
    209         assign_mapper(session.context, self.entity, self.entity.table, 
    210                       properties=properties, **kwargs) 
     210        if self.parent and self.inheritance == 'single': 
     211            args = [] 
     212        else: 
     213            args = [self.entity.table] 
     214 
     215        assign_mapper(session.context, self.entity, properties=properties,  
     216                      *args, **kwargs) 
     217 
     218    def _get_children(self): 
     219        children = self.children[:] 
     220        for child in self.children: 
     221            children.extend(child._descriptor._get_children()) 
     222        return children 
    211223 
    212224    def evaluate_property(self, prop): 
     
    240252        if self.parent: 
    241253            if self.inheritance == 'single': 
    242                 # reuse the parent's table 
    243                 if not self.parent.table: 
    244                     self.parent._descriptor.setup_table() 
    245                      
     254                # we know the parent is setup before the child 
    246255                self.entity.table = self.parent.table  
    247256 
     
    253262 
    254263                return 
    255 #            elif self.inheritance == 'concrete': 
    256                 # do not reuse parent table, but copy all fields 
    257                 # the problem is that, at this point, all "plain" fields 
    258                 # are known, but not those generated by relations 
    259 #                for field in self.fields.itervalues(): 
    260 #                    self.add_field(field) 
     264            elif self.inheritance == 'concrete': 
     265               # copy all fields from parent table 
     266               for field in self.parent._descriptor.fields.itervalues(): 
     267                    self.add_field(field.copy()) 
     268 
     269        if self.polymorphic and self.inheritance in ('single', 'multi') and \ 
     270           self.children and not self.parent: 
     271            if not isinstance(self.polymorphic, basestring): 
     272                self.polymorphic = DEFAULT_POLYMORPHIC_COL_NAME 
     273                 
     274            self.add_field(Field(DEFAULT_POLYMORPHIC_COL_TYPE,  
     275                                 colname=self.polymorphic)) 
    261276 
    262277        if self.version_id_col: 
     
    264279                self.version_id_col = DEFAULT_VERSION_ID_COL 
    265280            self.add_field(Field(Integer, colname=self.version_id_col)) 
    266  
    267         if not self.autoload: 
    268             if not self.has_pk and self.auto_primarykey: 
    269                 self.create_auto_primary_key() 
    270281 
    271282        # create list of columns and constraints 
     
    283294 
    284295 
     296    def create_pk_cols(self): 
     297        """ 
     298        Create primary_key columns. That is, add columns from belongs_to 
     299        relationships marked as being a primary_key and then adds a primary  
     300        key to the table if it hasn't already got one and needs one.  
     301         
     302        This method is "semi-recursive" in that it calls the create_keys  
     303        method on BelongsTo relationships and those in turn call create_pk_cols 
     304        on their target. It shouldn't be possible to have an infinite loop  
     305        since a loop of primary_keys is not a valid situation. 
     306        """ 
     307        for rel in self.relationships.itervalues(): 
     308            rel.create_keys(True) 
     309 
     310        if not self.autoload: 
     311            if self.parent and self.inheritance == 'multi': 
     312                # add foreign keys to the parent's primary key columns  
     313                parent_desc = self.parent._descriptor 
     314                for pk_col in parent_desc.primary_keys: 
     315                    colname = "%s_%s" % (self.parent.__name__.lower(), 
     316                                         pk_col.name) 
     317                    field = Field(pk_col.type, ForeignKey(pk_col),  
     318                                  colname=colname, primary_key=True) 
     319                    self.add_field(field) 
     320            if not self.has_pk and self.auto_primarykey: 
     321                self.create_auto_primary_key() 
     322 
     323 
    285324    def create_auto_primary_key(self): 
    286325        ''' 
    287326        Creates a primary key 
    288327        ''' 
    289          
    290         assert not self.has_pk and self.auto_primarykey 
    291328         
    292329        if isinstance(self.auto_primarykey, basestring): 
     
    304341            self.has_pk = True 
    305342 
    306         table = self.entity.table 
     343        # we don't want to trigger setup_all too early 
     344        table = type.__getattribute__(self.entity, 'table') 
    307345        if table: 
    308346            table.append_column(field.column) 
     
    342380 
    343381    def primary_keys(self): 
    344         return [col for col in self.entity.table.primary_key.columns] 
     382        if self.autoload: 
     383            return [col for col in self.entity.table.primary_key.columns] 
     384        else: 
     385            return [field.column for field in self.fields.itervalues() if 
     386                    field.primary_key] 
    345387    primary_keys = property(primary_keys) 
    346388 
     
    354396    all_relationships = property(all_relationships) 
    355397 
    356     def setup_relationships(cls): 
    357         for relationship in list(EntityDescriptor.uninitialized_rels): 
    358             if relationship.setup(): 
    359                 EntityDescriptor.uninitialized_rels.remove(relationship) 
    360     setup_relationships = classmethod(setup_relationships) 
     398 
     399class TriggerProxy(object): 
     400    def __init__(self, class_, attrname, setupfunc): 
     401        self.class_ = class_ 
     402        self.attrname = attrname 
     403        self.setupfunc = setupfunc 
     404 
     405    def __getattr__(self, name): 
     406        self.setupfunc() 
     407        proxied_attr = getattr(self.class_, self.attrname) 
     408        return getattr(proxied_attr, name) 
     409 
     410    def __repr__(self): 
     411        proxied_attr = getattr(self.class_, self.attrname) 
     412        return "<TriggerProxy (%s)>" % (self.class_.__name__) 
    361413 
    362414class EntityMeta(type): 
     
    366418    entities (ie you don't want to use the provided 'Entity' class). 
    367419    """ 
     420    _ready = False 
     421    _entities = {} 
    368422 
    369423    def __init__(cls, name, bases, dict_): 
     
    372426            return 
    373427 
     428        cid = cls._caller = id(sys._getframe(1)) 
     429        caller_entities = EntityMeta._entities.setdefault(cid, {}) 
     430        caller_entities[name] = cls 
     431 
    374432        # create the entity descriptor 
    375433        desc = cls._descriptor = EntityDescriptor(cls) 
    376         EntityDescriptor.current = desc 
    377  
    378         # process statements 
    379         Statement.process(cls) 
     434 
     435        # process statements. Needed before the proxy for metadata 
     436        Statement.process(cls, 'init') 
    380437 
    381438        # setup misc options here (like tablename etc.) 
    382439        desc.setup_options() 
    383440 
    384         # create table & assign (empty) mapper 
    385         desc.setup() 
     441        # create trigger proxies 
     442        # TODO: support entity_name... or maybe not. I'm not sure it makes  
     443        # sense in Elixir. 
     444        cls.setup_proxy() 
     445 
     446    def setup_proxy(cls, entity_name=None): 
     447        #TODO: move as much as possible of those "_private" values to the 
     448        # descriptor, so that we don't mess the initial class. 
     449        cls._class_key = sqlalchemy.orm.mapperlib.ClassKey(cls, entity_name) 
     450 
     451        tablename = cls._descriptor.tablename 
     452        schema = cls._descriptor.table_options.get('schema', None) 
     453        cls._table_key = sqlalchemy.schema._get_table_key(tablename, schema) 
     454 
     455        elixir._delayed_descriptors.append(cls._descriptor) 
     456         
     457        mapper_proxy = TriggerProxy(cls, 'mapper', elixir.setup_all) 
     458        table_proxy = TriggerProxy(cls, 'table', elixir.setup_all) 
     459 
     460        sqlalchemy.orm.mapper_registry[cls._class_key] = mapper_proxy 
     461        md = cls._descriptor.metadata 
     462        md.tables[cls._table_key] = table_proxy 
     463 
     464        # We need to monkeypatch the metadata's table iterator method because  
     465        # otherwise it doesn't work if the setup is triggered by the  
     466        # metadata.create_all(). 
     467        # This is because ManyToMany relationships add tables AFTER the list  
     468        # of tables that are going to be created is "computed"  
     469        # (metadata.tables.values()). 
     470        # see: 
     471        # - table_iterator method in MetaData class in sqlalchemy/schema.py  
     472        # - visit_metadata method in sqlalchemy/ansisql.py 
     473        original_table_iterator = md.table_iterator 
     474        if not hasattr(original_table_iterator,  
     475                       '_non_elixir_patched_iterator'): 
     476            def table_iterator(*args, **kwargs): 
     477                elixir.setup_all() 
     478                return original_table_iterator(*args, **kwargs) 
     479            table_iterator.__doc__ = original_table_iterator.__doc__ 
     480            table_iterator._non_elixir_patched_iterator = \ 
     481                original_table_iterator 
     482            md.table_iterator = table_iterator 
     483 
     484        cls._ready = True 
     485 
     486    def __getattribute__(cls, name): 
     487        if type.__getattribute__(cls, "_ready"): 
     488            #TODO: we need to add all assign_mapper methods 
     489            if name in ('c', 'table', 'mapper'): 
     490                elixir.setup_all() 
     491        return type.__getattribute__(cls, name) 
     492 
     493    def __call__(cls, *args, **kwargs): 
     494        elixir.setup_all() 
     495        return type.__call__(cls, *args, **kwargs) 
    386496 
    387497    def q(cls): 
     
    419529            setattr(self, key, value) 
    420530 
     531    def get_by(cls, *args, **kwargs): 
     532#        warnings.warn("The get_by method on the class is deprecated." 
     533#                      "You should use cls.query.get_by", DeprecationWarning, 
     534#                      stacklevel=2) 
     535        return cls.q.get_by(*args, **kwargs) 
     536    get_by = classmethod(get_by) 
     537 
     538    def select(cls, *args, **kwargs): 
     539#        warnings.warn("The select method on the class is deprecated." 
     540#                      "You should use cls.query.select", DeprecationWarning, 
     541#                      stacklevel=2) 
     542        return cls.q.select(*args, **kwargs) 
     543    select = classmethod(select) 
     544 
     545 
  • elixir/trunk/elixir/ext/versioned.py

    r172 r175  
    4343from elixir.statements     import Statement 
    4444from elixir.fields         import Field 
     45from sqlalchemy            import Table, Column, and_, desc 
     46from sqlalchemy.orm        import mapper 
    4547from sqlalchemy.orm.mapper import MapperExtension, EXT_PASS 
    46 from sqlalchemy            import Table, Column, mapper, and_, desc 
    4748from datetime              import datetime 
    4849 
  • elixir/trunk/elixir/fields.py

    r119 r175  
    9999        self.args = args 
    100100        self.kwargs = kwargs 
    101      
     101 
     102    def copy(self): 
     103        ''' 
     104        Returns a copy of the field 
     105        ''' 
     106 
     107        kwargs = self.kwargs 
     108        kwargs.update({'colname': self.colname, 'deferred': self.deferred}) 
     109        if 'nullable' in self.kwargs: 
     110            kwargs['required'] = not self.kwargs['nullable'] 
     111        return Field(self.type, *self.args, **kwargs) 
     112             
    102113    def column(self): 
    103114        ''' 
  • elixir/trunk/elixir/options.py

    r119 r175  
    3434| ``inheritance``     | Specify the type of inheritance this entity must use. | 
    3535|                     | It can be one of ``single``, ``concrete`` or          | 
    36 |                     | ``multi``.                                            | 
    37 |                     | **For now, only the single type is implemented.**     | 
     36|                     | ``multi``. Defaults to ``single``.                    | 
    3837+---------------------+-------------------------------------------------------+ 
    39 | ``metadata``        | Specify a custom MetaData                             | 
     38| ``polymorphic``     | Whether the inheritance should be polymorphic or not. | 
     39|                     | Defaults to ``False``. Note that polymorphic concrete | 
     40|                     | inheritance is currently not implemented.             | 
     41+---------------------+-------------------------------------------------------+ 
     42| ``metadata``        | Specify a custom MetaData.                            | 
    4043+---------------------+-------------------------------------------------------+ 
    4144| ``autoload``        | Automatically load column definitions from the        | 
    4245|                     | existing database table.                              | 
    43 |                     | Using autoloaded tables implies setting               | 
    44 |                     | ``delay_setup`` to ``True`` before defining your      | 
    45 |                     | entities.                                             | 
    4646+---------------------+-------------------------------------------------------+ 
    4747| ``tablename``       | Specify a custom tablename. You can either provide a  | 
     
    5555|                     | underscores ("_"), eg.: "project1_model_myentity"     | 
    5656|                     | for an entity named "MyEntity" in the module          | 
    57 |                     | "project1.model".  If shortnames is True, the         | 
     57|                     | "project1.model".  If shortnames is ``True``, the     | 
    5858|                     | tablename will just be the entity's classname         | 
    5959|                     | lower-cased, ie. "myentity".                          | 
     
    116116options_defaults = dict( 
    117117    inheritance='single', 
     118    polymorphic=False, 
    118119    autoload=None, 
    119120    shortnames=False, 
     
    128129    valid_options = ( 
    129130        'inheritance', 
    130         'metadata', 
     131        'polymorphic', 
    131132        'autoload', 
    132133        'tablename', 
     
    134135        'auto_primarykey', 
    135136        'version_id_col', 
     137        'metadata', 
    136138        'order_by', 
    137139    ) 
  • elixir/trunk/elixir/relationships.py

    r163 r175  
    199199from elixir.statements  import Statement 
    200200from elixir.fields      import Field 
    201 from elixir.entity      import EntityDescriptor 
     201from elixir.entity      import EntityDescriptor, EntityMeta 
    202202 
    203203import sys 
     
    228228        self.entity._descriptor.relationships[self.name] = self 
    229229     
    230     def create_keys(self): 
     230    def create_keys(self, pk): 
    231231        ''' 
    232232        Subclasses (ie. concrete relationships) may override this method to  
     
    250250        kwargs = {} 
    251251        if self.inverse: 
    252             # check if the inverse was already processed (and this has already defined 
    253             # a backref) 
     252            # check if the inverse was already processed (and thus has already 
     253            # defined a backref we can use) 
    254254            if self.inverse.backref: 
    255255                kwargs['backref'] = self.inverse.backref 
     
    269269        self.entity.mapper.add_property(self.name, self.property) 
    270270     
    271     def setup(self): 
    272         ''' 
    273         Sets up the relationship, creates foreign keys and secondary tables. 
    274         ''' 
    275  
    276         if not self.target: 
    277             return False 
    278  
    279         if self.property or self.backref: 
    280             return True 
    281  
    282         self.create_keys() 
    283         self.create_tables() 
    284         self.create_properties() 
    285          
    286         return True 
    287      
    288271    def target(self): 
    289272        if not self._target: 
     
    293276            if path: 
    294277                # do we have a fully qualified entity name? 
    295                 module = sys.modules.get(path.pop(), None) 
    296                 if module is None: 
    297                     # the module is probably not yet defined 
    298                     #TODO: in a delay_setup scenario, we should raise an 
    299                     #exception 
    300                     return None 
     278                module = sys.modules[path.pop()] 
     279                self._target = getattr(module, classname, None) 
    301280            else: 
    302                 # if not, try the same module as the source 
    303                 module = self.entity._descriptor.module 
    304  
    305             self._target = getattr(module, classname, None) 
    306             if not self._target: 
    307                 # This is ugly but we need it because the class which is 
    308                 # currently being defined (we have to keep in mind we are in  
    309                 # its metaclass code) is not yet available in the module 
    310                 # namespace, so the getattr above fails. And unfortunately, 
    311                 # this doesn't only happen for the owning entity of this 
    312                 # relation since we might be setting up a deferred relation. 
    313                 e = EntityDescriptor.current.entity 
    314                 if classname == e.__name__ or \ 
    315                         self.of_kind == e.__module__ +'.'+ e.__name__: 
    316                     self._target = e 
    317                 else: 
    318                     return None 
    319          
     281                # If not, try the list of entities of the "caller" of the  
     282                # source class. Most of the time, this will be the module the 
     283                # class is defined in. But it could also be a method (inner 
     284                # classes). 
     285                caller_entities = EntityMeta._entities[self.entity._caller] 
     286                self._target = caller_entities[classname] 
    320287        return self._target 
    321288    target = property(target) 
     
    386353        return isinstance(other, (HasMany, HasOne)) 
    387354 
    388     def create_keys(self): 
     355    def create_keys(self, pk): 
    389356        ''' 
    390357        Find all primary keys on the target and create foreign keys on the  
     
    394361            return 
    395362 
     363        if self.column_kwargs.get('primary_key', False) != pk: 
     364            return 
     365 
    396366        source_desc = self.entity._descriptor 
     367        #TODO: make this work if target is a pure SA-mapped class 
     368        # for that, I need:  
     369        # - the list of primary key columns of the target table 
     370        # - the name of the target table 
    397371        target_desc = self.target._descriptor 
     372        #make sure the target has all its pk setup up 
     373        target_desc.create_pk_cols() 
    398374 
    399375        if source_desc.autoload: 
     
    431407 
    432408                # we use a Field here instead of using a Column directly  
    433                 # because of add_field  
     409                # because add_field can be used before the table is created 
    434410                field = Field(pk_col.type, colname=colname, index=True,  
    435411                              **self.column_kwargs) 
     
    443419                fk_colnames.append(colname) 
    444420 
    445                 # build the list of columns the foreign key will point to 
    446                 if target_desc.entity.table.schema: 
    447                     fk_refcols.append("%s.%s.%s" % ( 
    448                         target_desc.entity.table.schema, 
    449                         target_desc.entity.table.name, 
    450                         pk_col.name)) 
    451                 else: 
    452                     fk_refcols.append("%s.%s" % (target_desc.entity.table.name, 
    453                                                  pk_col.name)) 
     421                # build the list of column "paths" the foreign key will  
     422                # point to 
     423                target_path = "%s.%s" % (target_desc.tablename, pk_col.name) 
     424                schema = target_desc.table_options.get('schema', None) 
     425                if schema is not None: 
     426                    target_path = "%s.%s" % (schema, target_path) 
     427                fk_refcols.append(target_path) 
    454428 
    455429                # build up the primary join. This is needed when you have  
     
    459433            # In some databases (at lease MySQL) the constraint name needs to  
    460434            # be unique for the whole database, instead of per table. 
    461             fk_name = "%s_%s_fk" % (self.entity.table.name,  
     435            fk_name = "%s_%s_fk" % (source_desc.tablename,  
    462436                                    '_'.join(fk_colnames)) 
    463437            source_desc.add_constraint(ForeignKeyConstraint( 
     
    487461        return isinstance(other, BelongsTo) 
    488462 
    489     def create_keys(self): 
     463    def create_keys(self, pk): 
    490464        # make sure an inverse relationship exists 
    491465        if self.inverse is None: 
     
    500474                         self.entity.__name__)) 
    501475        # make sure it is set up because it creates the foreign key we'll need 
    502         self.inverse.create_keys() 
     476        self.inverse.create_keys(pk) 
    503477     
    504478    def get_prop_kwargs(self): 
  • elixir/trunk/elixir/statements.py

    r164 r175  
    1212    ''' 
    1313     
    14     def __init__(self, target): 
     14    def __init__(self, target, when='init'): 
    1515        ''' 
    1616        target is the class which will handle this statement. For example, the 
     
    1818        ''' 
    1919        self.target = target 
     20        self.when = when 
    2021     
    2122    def __call__(self, *args, **kwargs): 
    22         # jam this statement into the class's statement list 
     23        # jam this statement into one of the class's statement lists 
    2324        class_locals = sys._getframe(1).f_locals 
    24         statements = class_locals.setdefault(STATEMENTS, []) 
     25        statement_map = class_locals.setdefault(STATEMENTS, {}) 
     26        statements = statement_map.setdefault(self.when, []) 
    2527        statements.append((self, args, kwargs)) 
    2628     
    2729    @classmethod 
    28     def finalize(cls, entity): 
    29         for statement, args, kwargs in getattr(entity, STATEMENTS, []): 
     30    def finalize(cls, entity, when='init'): 
     31        statement_map = getattr(entity, STATEMENTS, {}) 
     32        statements = statement_map.get(when, []) 
     33        for statement, args, kwargs in statements: 
    3034            getattr(statement.target, 'finalize', lambda x: None)(entity) 
    3135     
    3236    @classmethod 
    33     def process(cls, entity): 
     37    def process(cls, entity, when): 
    3438        ''' 
    3539        Apply all statements to the given entity. 
    3640        ''' 
    37         # loop over all statements in the class's statement list  
     41        # loop over all statements in the class's statement list named "when" 
    3842        # and apply them, i.e. instanciate the corresponding classes 
    39         for statement, args, kwargs in getattr(entity, STATEMENTS, []): 
     43        statement_map = getattr(entity, STATEMENTS, {}) 
     44        statements = statement_map.get(when, []) 
     45        for statement, args, kwargs in statements: 
    4046            statement.target(entity, *args, **kwargs) 
  • elixir/trunk/tests/test_autoload.py

    r142 r175  
    33""" 
    44 
    5 from sqlalchemy import Table, Column, ForeignKey, BoundMetaData, create_engine 
     5from sqlalchemy import Table, Column, ForeignKey, MetaData 
    66from elixir import * 
    77import elixir 
     
    99def setup(): 
    1010    # First create the tables (it would be better to use an external db) 
    11     engine = create_engine('sqlite:///') 
    12     meta = BoundMetaData(engine) 
     11    meta = MetaData('sqlite:///') 
    1312 
    1413    person_table = Table('person', meta, 
     
    3736    meta.create_all() 
    3837 
    39     elixir.delay_setup = True 
    4038    elixir.options_defaults.update(dict(autoload=True, shortnames=True)) 
    4139 
     
    6866                                tablename='person_category') 
    6967 
    70     elixir.delay_setup = False 
    7168    elixir.options_defaults.update(dict(autoload=False, shortnames=False)) 
    7269 
    73     metadata.connect(engine) 
     70    metadata.connect(meta.bind) 
    7471    setup_all() 
    7572 
     
    118115        objectstore.clear() 
    119116         
    120         p = Person.get_by(name="Homer") 
    121          
    122         print "%s is %s's child." % (p.name, p.father.name) 
    123         print "His children are: %s." % ( 
    124                 " and ".join(c.name for c in p.children)) 
     117        p = Person.q.filter_by(name="Homer").one() 
    125118         
    126119        assert p in p.father.children 
     120        assert p.father.name == "Abe" 
    127121        assert p.father is Person.get_by(name="Abe") 
    128122        assert p is Person.get_by(name="Lisa").father 
  • elixir/trunk/tests/test_autoload_mixed.py

    r145 r175  
    55 
    66def teardown(): 
    7     cleanup_all() 
     7    cleanup_all(True) 
    88 
    99class TestAutoloadMixed(object): 
    1010    def setup(self): 
    11         conn = metadata.engine.connect() 
     11        conn = metadata.bind.connect() 
    1212        conn.execute("""CREATE TABLE user 
    1313        (user_id INTEGER PRIMARY KEY AUTOINCREMENT)""") 
     
    1515         
    1616    def test_belongs_to(self): 
    17         global User, Item 
    18  
    1917        class User(Entity): 
    2018            using_options(tablename='user', autoload=True) 
     
    2321            belongs_to('owner', of_kind='User') 
    2422 
    25         create_all() 
     23        setup_all(True) 
    2624 
    2725        assert Item.table.c['owner_user_id'].foreign_key.column.name == 'user_id' 
  • elixir/trunk/tests/test_autoload_nopk.py

    r145 r175  
    1616class TestAutoload(object): 
    1717    def test_pk(self): 
    18         local_meta = MetaData(metadata.engine) 
     18        local_meta = MetaData(metadata.bind) 
    1919 
    2020        person_table = Table('person', local_meta, 
  • elixir/trunk/tests/test_has_property.py

    r146 r175  
    33""" 
    44 
    5 from sqlalchemy import column_property 
     5from sqlalchemy.orm import column_property 
    66from elixir import * 
    77 
  • elixir/trunk/tests/test_inherit.py

    r146 r175  
    66 
    77def setup(): 
    8     global Person, PersonExtended 
    9  
    10     class Person(Entity): 
    11         has_field('firstname', Unicode(30)) 
    12         has_field('surname', Unicode(30)) 
    13         belongs_to('sister', of_kind='Person') 
    14  
    15         @property 
    16         def name(self): 
    17             return "%s %s" % (self.firstname, self.surname) 
    18  
    19         def __str__(self): 
    20             sister = self.sister and self.sister.name or "unknown" 
    21             return "%s [%s]" % (self.name, sister) 
    22      
    23     class PersonExtended(Person): 
    24         has_field('age', Integer) 
    25         belongs_to('parent', of_kind='PersonExtended') 
    26  
    27         using_options(inheritance='single') 
    28  
    29         def __str__(self): 
    30             parent = self.parent and self.parent.name or "unknown" 
    31             return "%s (%s) {%s}" % (super(PersonExtended, self).__str__(),  
    32                                      self.age, parent) 
    33  
    348    metadata.connect('sqlite:///') 
    359 
     10def do_tst(inheritance, polymorphic, expected_res): 
     11    class A(Entity): 
     12        has_field('data1', String(20)) 
     13        using_options(inheritance=inheritance, polymorphic=polymorphic) 
    3614 
    37 def teardown(): 
    38     cleanup_all() 
     15    class B(A): 
     16        has_field('data2', String(20)) 
     17        using_options(inheritance=inheritance, polymorphic=polymorphic) 
     18 
     19    class C(B): 
     20        has_field('data3', String(20)) 
     21        using_options(inheritance=inheritance, polymorphic=polymorphic) 
     22 
     23    class D(A): 
     24        has_field('data4', String(20)) 
     25        using_options(inheritance=inheritance, polymorphic=polymorphic) 
     26 
     27    setup_all(True) 
     28 
     29    A(data1='a1') 
     30    B(data1='b1', data2='b2') 
     31    C(data1='c1', data2='c2', data3='c3') 
     32    D(data1='d1', data4='d4') 
     33 
     34    objectstore.flush() 
     35    objectstore.clear() 
     36 
     37    res = {} 
     38    for class_ in (A, B, C, D): 
     39        res[class_.__name__] = class_.q.all() 
     40        res[class_.__name__].sort(key=lambda o: o.__class__.__name__)  
     41 
     42    for query_class in ('A', 'B', 'C', 'D'): 
     43        assert len(res[query_class]) == len(expected_res[query_class]) 
     44        for real, expected in zip(res[query_class], expected_res[query_class]): 
     45            assert real.__class__.__name__ == expected 
    3946 
    4047 
    4148class TestInheritance(object): 
    42     def setup(self): 
    43         create_all() 
    44      
    4549    def teardown(self): 
    46         drop_all() 
    47         objectstore.clear() 
     50        cleanup_all(True) 
    4851 
    4952    def test_singletable_inheritance(self): 
    50         homer = PersonExtended(firstname="Homer", surname="Simpson", age=36) 
    51         # lisa needs to be a Person object, not a PersonExtended object because 
    52         # the sister relationship points to a Person, not a PersonExtended, so 
    53         # bart's sister must be a Person. This is to comply with SQLAlchemy's 
    54         # policy to prevent loading relationships with unintended types, unless  
    55         # explicitly enabled (enable_typechecks=False). 
    56         lisa = Person(firstname="Lisa", surname="Simpson") 
    57         bart = PersonExtended(firstname="Bart", surname="Simpson",  
    58                               parent=homer, sister=lisa) 
     53        do_tst('single', False, { 
     54            'A': ('A', 'A', 'A', 'A'), 
     55            'B': ('B', 'B', 'B', 'B'), 
     56            'C': ('C', 'C', 'C', 'C'), 
     57            'D': ('D', 'D', 'D', 'D'), 
     58        }) 
    5959 
    60         objectstore.flush() 
    61         objectstore.clear() 
     60    def test_polymorphic_singletable_inheritance(self): 
     61        do_tst('single', True, { 
     62            'A': ('A', 'B', 'C', 'D'), 
     63            'B': ('B', 'C'), 
     64            'C': ('C',), 
     65            'D': ('D',), 
     66        }) 
    6267 
    63         p = PersonExtended.get_by(firstname="Bart") 
     68    def test_concrete_inheritance(self): 
     69        do_tst('concrete', False, { 
     70            'A': ('A',), 
     71            'B': ('B',), 
     72            'C': ('C',), 
     73            'D': ('D',), 
     74        }) 
    6475 
    65         assert p.sister.name == 'Lisa Simpson' 
    66         assert p.parent.age == 36 
     76    def test_multitable_inheritance(self): 
     77        do_tst('multi', False, { 
     78            'A': ('A', 'A', 'A', 'A'), 
     79            'B': ('B', 'B'), 
     80            'C': ('C',), 
     81            'D': ('D',), 
     82        }) 
    6783 
    68         for p in Person.select(): 
    69             print p 
     84    def test_polymorphic_multitable_inheritance(self): 
     85        do_tst('multi', True, { 
     86            'A': ('A', 'B', 'C', 'D'), 
     87            'B': ('B', 'C'), 
     88            'C': ('C',), 
     89            'D': ('D',), 
     90        }) 
    7091 
    71         for p in PersonExtended.select(): 
    72             print p 
    73  
    74 if __name__ == '__main__': 
    75     setup() 
    76     test = TestInheritance() 
    77     test.setup() 
    78     test.test_singletable_inheritance() 
    79     test.teardown() 
    80     teardown() 
  • elixir/trunk/tests/test_options.py

    r146 r175  
    33""" 
    44 
    5 from sqlalchemy import create_session, UniqueConstraint  
     5from sqlalchemy import UniqueConstraint  
     6from sqlalchemy.orm import create_session 
    67from sqlalchemy.exceptions import SQLError, ConcurrentModificationError  
    78from elixir import * 
     
    8485        cleanup_all() 
    8586 
    86     def test_table_options(self): 
     87    def test_unique_constraint(self): 
     88         
    8789        class Person(Entity): 
    8890            has_field('firstname', Unicode(30)) 
     
    108110        assert raised 
    109111 
     112    def test_unique_constraint_belongs_to(self): 
     113        class Author(Entity): 
     114            has_field("name", Unicode) 
     115 
     116        class Book(Entity): 
     117            has_field("title", Unicode, required=True) 
     118            belongs_to("author", of_kind="Author") 
     119 
     120            using_table_options(UniqueConstraint("title", "author_id")) 
     121 
     122        setup_all(True) 
     123 
     124        tolkien = Author(name="J. R. R. Tolkien") 
     125        lotr = Book(title="The Lord of the Rings", author=tolkien) 
     126        hobbit = Book(title="The Hobbit", author=tolkien) 
     127 
     128        objectstore.flush() 
     129 
     130        tolkien2 = Author(name="Tolkien") 
     131        hobbit2 = Book(title="The Hobbit", author=tolkien2) 
     132 
     133        objectstore.flush() 
     134 
     135        hobbit3 = Book(title="The Hobbit", author=tolkien) 
     136 
     137        raised = False 
     138        try: 
     139            objectstore.flush() 
     140        except SQLError: 
     141            raised = True 
     142 
     143        assert raised 
     144