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

merged autodelay branch!

Files:
1 modified

Legend:

Unmodified
Added
Removed
  • 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