root / elixir / trunk / elixir / entity.py @ 164

Revision 164, 12.9 kB (checked in by cleverdevil, 6 years ago)

Added some hooks to allow Elixir statements to perform some post-setup "finalization" which will be required for some upcoming Elixir extensions that I have in the works. Statements need only define a "finalize" class method that takes in an entity.

Line 
1'''
2Entity baseclass, metaclass and descriptor
3'''
4
5from sqlalchemy                     import Table, Integer, desc
6from sqlalchemy.orm                 import deferred, Query
7from sqlalchemy.ext.assignmapper    import assign_mapper
8from sqlalchemy.util                import OrderedDict
9from elixir.statements              import Statement
10from elixir.fields                  import Field
11from elixir.options                 import options_defaults
12
13try:
14    set
15except NameError:
16    from sets import Set as set
17
18import sys
19import elixir
20
21__pudge_all__ = ['Entity', 'EntityMeta']
22
23DEFAULT_AUTO_PRIMARYKEY_NAME = "id"
24DEFAULT_AUTO_PRIMARYKEY_TYPE = Integer
25DEFAULT_VERSION_ID_COL = "row_version"
26
27class EntityDescriptor(object):
28    '''
29    EntityDescriptor describes fields and options needed for table creation.
30    '''
31   
32    uninitialized_rels = set()
33    current = None
34   
35    def __init__(self, entity):
36        entity.table = None
37        entity.mapper = None
38
39        self.entity = entity
40        self.module = sys.modules[entity.__module__]
41
42        self.has_pk = False
43
44        self.parent = None
45        for base in entity.__bases__:
46            if issubclass(base, Entity) and base is not Entity:
47                if self.parent:
48                    raise Exception('%s entity inherits from several entities,'
49                                    ' and this is not supported.' 
50                                    % self.entity.__name__)
51                else:
52                    self.parent = base
53
54        self.fields = OrderedDict()
55        self.relationships = dict()
56        self.delayed_properties = dict()
57        self.constraints = list()
58
59        #CHECKME: this is a workaround for the "current" descriptor/target
60        # property ugliness. The problem is that this workaround is ugly too.
61        # I'm not sure if this is a safe practice. It works but...?
62#        setattr(self.module, entity.__name__, entity)
63
64        # set default value for options
65        self.order_by = None
66        self.table_args = list()
67        self.metadata = getattr(self.module, 'metadata', elixir.metadata)
68
69        for option in ('inheritance', 
70                       'autoload', 'tablename', 'shortnames', 
71                       'auto_primarykey',
72                       'version_id_col'):
73            setattr(self, option, options_defaults[option])
74
75        for option_dict in ('mapper_options', 'table_options'):
76            setattr(self, option_dict, options_defaults[option_dict].copy())
77   
78    def setup_options(self):
79        '''
80        Setup any values that might depend on using_options. For example, the
81        tablename or the metadata.
82        '''
83        elixir.metadatas.add(self.metadata)
84
85        entity = self.entity
86
87        if not self.tablename:
88            if self.shortnames:
89                self.tablename = entity.__name__.lower()
90            else:
91                modulename = entity.__module__.replace('.', '_')
92                tablename = "%s_%s" % (modulename, entity.__name__)
93                self.tablename = tablename.lower()
94        elif callable(self.tablename):
95            self.tablename = self.tablename(entity)
96   
97    def setup(self):
98        '''
99        Create tables, keys, columns that have been specified so far and
100        assign a mapper. Will be called when an instance of the entity is
101        created or a mapper is needed to access one or many instances of the
102        entity. It will try to initialize the entity's relationships (along
103        with any delayed relationship) but some of them might be delayed.
104        '''
105        if elixir.delay_setup:
106            elixir.delayed_entities.add(self)
107            return
108
109        self.setup_table()
110        self.setup_mapper()
111
112        # This marks all relations of the entity (or, at least those which
113        # have been added so far by statements) as being uninitialized
114        EntityDescriptor.uninitialized_rels.update(
115            self.relationships.values())
116
117        # try to setup all uninitialized relationships
118        EntityDescriptor.setup_relationships()
119       
120        # finally, allow the statement to do any "finalization"
121        Statement.finalize(self.entity)
122   
123    def translate_order_by(self, order_by):
124        if isinstance(order_by, basestring):
125            order_by = [order_by]
126       
127        order = list()
128        for field in order_by:
129            col = self.fields[field.strip('-')].column
130            if field.startswith('-'):
131                col = desc(col)
132            order.append(col)
133        return order
134
135    def setup_mapper(self):
136        '''
137        Initializes and assign an (empty!) mapper to the entity.
138        '''
139        if self.entity.mapper:
140            return
141       
142        session = getattr(self.module, 'session', elixir.objectstore)
143       
144        kwargs = self.mapper_options
145        if self.order_by:
146            kwargs['order_by'] = self.translate_order_by(self.order_by)
147       
148        if self.version_id_col:
149            kwargs['version_id_col'] = self.fields[self.version_id_col].column
150
151        if self.parent:
152            if self.inheritance == 'single':
153                # at this point, we don't know whether the parent relationships
154                # have already been processed or not. Some of them might be,
155                # some other might not.
156                if not self.parent.mapper:
157                    self.parent._descriptor.setup_mapper()
158                kwargs['inherits'] = self.parent.mapper
159
160        properties = dict()
161        for field in self.fields.itervalues():
162            if field.deferred:
163                group = None
164                if isinstance(field.deferred, basestring):
165                    group = field.deferred
166                properties[field.column.name] = deferred(field.column,
167                                                         group=group)
168
169        #TODO: make this happen after the rel columns have been added.
170        for name, prop in self.delayed_properties.iteritems():
171            properties[name] = self.evaluate_property(prop)
172        self.delayed_properties.clear()
173
174        if 'primary_key' in kwargs:
175            cols = self.entity.table.c
176            kwargs['primary_key'] = [getattr(cols, colname) for
177                colname in kwargs['primary_key']]
178
179        assign_mapper(session.context, self.entity, self.entity.table,
180                      properties=properties, **kwargs)
181
182    def evaluate_property(self, prop):
183        if callable(prop):
184            return prop(self.entity.table.c)
185        else:
186            return prop
187
188    def add_property(self, name, prop):
189        if self.entity.mapper:
190            prop_value = self.evaluate_property(prop)
191            self.entity.mapper.add_property(name, prop_value)
192        else:
193            self.delayed_properties[name] = prop
194
195    def setup_table(self):
196        '''
197        Create a SQLAlchemy table-object with all columns that have been
198        defined up to this point.
199        '''
200        if self.entity.table:
201            return
202       
203        if self.parent:
204            if self.inheritance == 'single':
205                # reuse the parent's table
206                if not self.parent.table:
207                    self.parent._descriptor.setup_table()
208                   
209                self.entity.table = self.parent.table 
210
211                # re-add the entity fields to the parent entity so that they
212                # are added to the parent's table (whether the parent's table
213                # is already setup or not).
214                for field in self.fields.itervalues():
215                    self.parent._descriptor.add_field(field)
216
217                return
218#            elif self.inheritance == 'concrete':
219                # do not reuse parent table, but copy all fields
220                # the problem is that, at this point, all "plain" fields
221                # are known, but not those generated by relations
222#                for field in self.fields.itervalues():
223#                    self.add_field(field)
224
225        if self.version_id_col:
226            if not isinstance(self.version_id_col, basestring):
227                self.version_id_col = DEFAULT_VERSION_ID_COL
228            self.add_field(Field(Integer, colname=self.version_id_col))
229
230        if not self.autoload:
231            if not self.has_pk and self.auto_primarykey:
232                self.create_auto_primary_key()
233
234        # create list of columns and constraints
235        args = [field.column for field in self.fields.itervalues()] \
236                    + self.constraints + self.table_args
237       
238        # specify options
239        kwargs = self.table_options
240
241        if self.autoload:
242            kwargs['autoload'] = True
243       
244        self.entity.table = Table(self.tablename, self.metadata, 
245                                  *args, **kwargs)
246
247
248    def create_auto_primary_key(self):
249        '''
250        Creates a primary key
251        '''
252       
253        assert not self.has_pk and self.auto_primarykey
254       
255        if isinstance(self.auto_primarykey, basestring):
256            colname = self.auto_primarykey
257        else:
258            colname = DEFAULT_AUTO_PRIMARYKEY_NAME
259       
260        self.add_field(Field(DEFAULT_AUTO_PRIMARYKEY_TYPE,
261                             colname=colname, primary_key=True))
262       
263    def add_field(self, field):
264        self.fields[field.colname] = field
265       
266        if field.primary_key:
267            self.has_pk = True
268
269        table = self.entity.table
270        if table:
271            table.append_column(field.column)
272   
273    def add_constraint(self, constraint):
274        self.constraints.append(constraint)
275       
276        table = self.entity.table
277        if table:
278            table.append_constraint(constraint)
279       
280    def get_inverse_relation(self, rel, reverse=False):
281        '''
282        Return the inverse relation of rel, if any, None otherwise.
283        '''
284
285        matching_rel = None
286        for other_rel in self.relationships.itervalues():
287            if other_rel.is_inverse(rel):
288                if matching_rel is None:
289                    matching_rel = other_rel
290                else:
291                    raise Exception(
292                            "Several relations match as inverse of the '%s' "
293                            "relation in entity '%s'. You should specify "
294                            "inverse relations manually by using the inverse "
295                            "keyword."
296                            % (rel.name, rel.entity.__name__))
297        # When a matching inverse is found, we check that it has only
298        # one relation matching as its own inverse. We don't need the result
299        # of the method though. But we do need to be careful not to start an
300        # infinite recursive loop.
301        if matching_rel and not reverse:
302            rel.entity._descriptor.get_inverse_relation(matching_rel, True)
303
304        return matching_rel
305
306    def primary_keys(self):
307        return [col for col in self.entity.table.primary_key.columns]
308    primary_keys = property(primary_keys)
309
310    def all_relationships(self):
311        if self.parent:
312            res = self.parent._descriptor.all_relationships
313        else:
314            res = dict()
315        res.update(self.relationships)
316        return res
317    all_relationships = property(all_relationships)
318
319    def setup_relationships(cls):
320        for relationship in list(EntityDescriptor.uninitialized_rels):
321            if relationship.setup():
322                EntityDescriptor.uninitialized_rels.remove(relationship)
323    setup_relationships = classmethod(setup_relationships)
324
325class EntityMeta(type):
326    """
327    Entity meta class.
328    You should only use this if you want to define your own base class for your
329    entities (ie you don't want to use the provided 'Entity' class).
330    """
331
332    def __init__(cls, name, bases, dict_):
333        # only process subclasses of Entity, not Entity itself
334        if bases[0] is object:
335            return
336
337        # create the entity descriptor
338        desc = cls._descriptor = EntityDescriptor(cls)
339        EntityDescriptor.current = desc
340
341        # process statements
342        Statement.process(cls)
343
344        # setup misc options here (like tablename etc.)
345        desc.setup_options()
346
347        # create table & assign (empty) mapper
348        desc.setup()
349
350    def q(cls):
351        return Query(cls, session=elixir.objectstore.session)
352    q = property(q)
353
354
355class Entity(object):
356    '''
357    The base class for all entities
358   
359    All Elixir model objects should inherit from this class. Statements can
360    appear within the body of the definition of an entity to define its
361    fields, relationships, and other options.
362   
363    Here is an example:
364
365    ::
366   
367        class Person(Entity):
368            has_field('name', Unicode(128))
369            has_field('birthdate', DateTime, default=datetime.now)
370   
371    Please note, that if you don't specify any primary keys, Elixir will
372    automatically create one called ``id``.
373   
374    For further information, please refer to the provided examples or
375    tutorial.
376    '''
377
378    __metaclass__ = EntityMeta
379
380    def __init__(self, **kwargs):
381        for key, value in kwargs.items():
382            setattr(self, key, value)
Note: See TracBrowser for help on using the browser.