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

Revision 139, 12.6 kB (checked in by ged, 7 years ago)

- Applied patch from Ants Aasma to make Elixir compatible with the 0.4 branch

of SQLAlchemy.

Line 
1'''
2Entity baseclass, metaclass and descriptor
3'''
4
5from sqlalchemy                     import Table, Integer, desc
6from sqlalchemy.orm                 import deferred
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.primary_keys = list()
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    def translate_order_by(self, order_by):
121        if isinstance(order_by, basestring):
122            order_by = [order_by]
123       
124        order = list()
125        for field in order_by:
126            col = self.fields[field.strip('-')].column
127            if field.startswith('-'):
128                col = desc(col)
129            order.append(col)
130        return order
131
132    def setup_mapper(self):
133        '''
134        Initializes and assign an (empty!) mapper to the entity.
135        '''
136        if self.entity.mapper:
137            return
138       
139        session = getattr(self.module, 'session', elixir.objectstore)
140       
141        kwargs = self.mapper_options
142        if self.order_by:
143            kwargs['order_by'] = self.translate_order_by(self.order_by)
144       
145        if self.version_id_col:
146            kwargs['version_id_col'] = self.fields[self.version_id_col].column
147
148        if self.parent:
149            if self.inheritance == 'single':
150                # at this point, we don't know whether the parent relationships
151                # have already been processed or not. Some of them might be,
152                # some other might not.
153                if not self.parent.mapper:
154                    self.parent._descriptor.setup_mapper()
155                kwargs['inherits'] = self.parent.mapper
156
157        properties = dict()
158        for field in self.fields.itervalues():
159            if field.deferred:
160                group = None
161                if isinstance(field.deferred, basestring):
162                    group = field.deferred
163                properties[field.column.name] = deferred(field.column,
164                                                         group=group)
165
166        #TODO: make this happen after the rel columns have been added.
167        for name, prop in self.delayed_properties.iteritems():
168            properties[name] = self.evaluate_property(prop)
169        self.delayed_properties.clear()
170
171        assign_mapper(session.context, self.entity, self.entity.table,
172                      properties=properties, **kwargs)
173
174    def evaluate_property(self, prop):
175        if callable(prop):
176            return prop(self.entity.table.c)
177        else:
178            return prop
179
180    def add_property(self, name, prop):
181        if self.entity.mapper:
182            prop_value = self.evaluate_property(prop)
183            self.entity.mapper.add_property(name, prop_value)
184        else:
185            self.delayed_properties[name] = prop
186
187    def setup_table(self):
188        '''
189        Create a SQLAlchemy table-object with all columns that have been
190        defined up to this point.
191        '''
192        if self.entity.table:
193            return
194       
195        if self.parent:
196            if self.inheritance == 'single':
197                # reuse the parent's table
198                if not self.parent.table:
199                    self.parent._descriptor.setup_table()
200                   
201                self.entity.table = self.parent.table 
202                self.primary_keys = self.parent._descriptor.primary_keys
203
204                # re-add the entity fields to the parent entity so that they
205                # are added to the parent's table (whether the parent's table
206                # is setup already or not).
207                for field in self.fields.itervalues():
208                    self.parent._descriptor.add_field(field)
209
210                return
211#            elif self.inheritance == 'concrete':
212                # do not reuse parent table, but copy all fields
213                # the problem is that, at this points, all "plain" fields
214                # are known, but not those generated by relations
215#                for field in self.fields.itervalues():
216#                    self.add_field(field)
217
218        if self.version_id_col:
219            if not isinstance(self.version_id_col, basestring):
220                self.version_id_col = DEFAULT_VERSION_ID_COL
221            self.add_field(Field(Integer, colname=self.version_id_col))
222
223        if not self.autoload:
224            if not self.primary_keys and self.auto_primarykey:
225                self.create_auto_primary_key()
226
227        # create list of columns and constraints
228        args = [field.column for field in self.fields.itervalues()] \
229                    + self.constraints + self.table_args
230       
231        # specify options
232        kwargs = self.table_options
233
234        if self.autoload:
235            kwargs['autoload'] = True
236       
237        self.entity.table = Table(self.tablename, self.metadata, 
238                                  *args, **kwargs)
239   
240    def create_auto_primary_key(self):
241        '''
242        Creates a primary key
243        '''
244       
245        assert not self.primary_keys and self.auto_primarykey
246       
247        if isinstance(self.auto_primarykey, basestring):
248            colname = self.auto_primarykey
249        else:
250            colname = DEFAULT_AUTO_PRIMARYKEY_NAME
251       
252        self.add_field(Field(DEFAULT_AUTO_PRIMARYKEY_TYPE,
253                             colname=colname, primary_key=True))
254       
255    def add_field(self, field):
256        self.fields[field.colname] = field
257       
258        if field.primary_key:
259            self.primary_keys.append(field)
260       
261        table = self.entity.table
262        if table:
263            table.append_column(field.column)
264   
265    def add_constraint(self, constraint):
266        self.constraints.append(constraint)
267       
268        table = self.entity.table
269        if table:
270            table.append_constraint(constraint)
271       
272    def get_inverse_relation(self, rel, reverse=False):
273        '''
274        Return the inverse relation of rel, if any, None otherwise.
275        '''
276
277        matching_rel = None
278        for other_rel in self.relationships.itervalues():
279            if other_rel.is_inverse(rel):
280                if matching_rel is None:
281                    matching_rel = other_rel
282                else:
283                    raise Exception(
284                            "Several relations match as inverse of the '%s' "
285                            "relation in entity '%s'. You should specify "
286                            "inverse relations manually by using the inverse "
287                            "keyword."
288                            % (rel.name, rel.entity.__name__))
289        # When a matching inverse is found, we check that it has only
290        # one relation matching as its own inverse. We don't need the result
291        # of the method though. But we do need to be careful not to start an
292        # infinite recursive loop.
293        if matching_rel and not reverse:
294            rel.entity._descriptor.get_inverse_relation(matching_rel, True)
295
296        return matching_rel
297
298    def all_relationships(self):
299        if self.parent:
300            res = self.parent._descriptor.all_relationships
301        else:
302            res = dict()
303        res.update(self.relationships)
304        return res
305    all_relationships = property(all_relationships)
306
307    def setup_relationships(cls):
308        for relationship in list(EntityDescriptor.uninitialized_rels):
309            if relationship.setup():
310                EntityDescriptor.uninitialized_rels.remove(relationship)
311    setup_relationships = classmethod(setup_relationships)
312
313class EntityMeta(type):
314    """
315    Entity meta class.
316    You should only use this if you want to define your own base class for your
317    entities (ie you don't want to use the provided 'Entity' class).
318    """
319
320    def __init__(cls, name, bases, dict_):
321        # only process subclasses of Entity, not Entity itself
322        if bases[0] is object:
323            return
324
325        # create the entity descriptor
326        desc = cls._descriptor = EntityDescriptor(cls)
327        EntityDescriptor.current = desc
328       
329        # process statements
330        Statement.process(cls)
331       
332        # setup misc options here (like tablename etc.)
333        desc.setup_options()
334       
335        # create table & assign (empty) mapper
336        desc.setup()
337
338    def q(cls):
339        return cls.query()
340    q = property(q)
341
342class Entity(object):
343    '''
344    The base class for all entities
345   
346    All Elixir model objects should inherit from this class. Statements can
347    appear within the body of the definition of an entity to define its
348    fields, relationships, and other options.
349   
350    Here is an example:
351
352    ::
353   
354        class Person(Entity):
355            has_field('name', Unicode(128))
356            has_field('birthdate', DateTime, default=datetime.now)
357   
358    Please note, that if you don't specify any primary keys, Elixir will
359    automatically create one called ``id``.
360   
361    For further information, please refer to the provided examples or
362    tutorial.
363    '''
364
365    __metaclass__ = EntityMeta
366
367    def __init__(self, **kwargs):
368         for key, value in kwargs.items():
369             setattr(self, key, value)
Note: See TracBrowser for help on using the browser.