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

Revision 148, 12.9 kB (checked in by ged, 6 years ago)

do not rely on the query() classmethod to provide the .q property

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    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        if 'primary_key' in kwargs:
172            cols = self.entity.table.c
173            kwargs['primary_key'] = [getattr(cols, colname) for
174                colname in kwargs['primary_key']]
175
176        assign_mapper(session.context, self.entity, self.entity.table,
177                      properties=properties, **kwargs)
178
179    def evaluate_property(self, prop):
180        if callable(prop):
181            return prop(self.entity.table.c)
182        else:
183            return prop
184
185    def add_property(self, name, prop):
186        if self.entity.mapper:
187            prop_value = self.evaluate_property(prop)
188            self.entity.mapper.add_property(name, prop_value)
189        else:
190            self.delayed_properties[name] = prop
191
192    def setup_table(self):
193        '''
194        Create a SQLAlchemy table-object with all columns that have been
195        defined up to this point.
196        '''
197        if self.entity.table:
198            return
199       
200        if self.parent:
201            if self.inheritance == 'single':
202                # reuse the parent's table
203                if not self.parent.table:
204                    self.parent._descriptor.setup_table()
205                   
206                self.entity.table = self.parent.table 
207
208                # re-add the entity fields to the parent entity so that they
209                # are added to the parent's table (whether the parent's table
210                # is already setup or not).
211                for field in self.fields.itervalues():
212                    self.parent._descriptor.add_field(field)
213
214                return
215#            elif self.inheritance == 'concrete':
216                # do not reuse parent table, but copy all fields
217                # the problem is that, at this points, all "plain" fields
218                # are known, but not those generated by relations
219#                for field in self.fields.itervalues():
220#                    self.add_field(field)
221
222        if self.version_id_col:
223            if not isinstance(self.version_id_col, basestring):
224                self.version_id_col = DEFAULT_VERSION_ID_COL
225            self.add_field(Field(Integer, colname=self.version_id_col))
226
227        if not self.autoload:
228            if not self.has_pk and self.auto_primarykey:
229                self.create_auto_primary_key()
230
231        # create list of columns and constraints
232        args = [field.column for field in self.fields.itervalues()] \
233                    + self.constraints + self.table_args
234       
235        # specify options
236        kwargs = self.table_options
237
238        if self.autoload:
239            kwargs['autoload'] = True
240       
241        self.entity.table = Table(self.tablename, self.metadata, 
242                                  *args, **kwargs)
243   
244    def primary_keys(self):
245        return [col for col in self.entity.table.primary_key.columns]
246    primary_keys = property(primary_keys)
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 all_relationships(self):
307        if self.parent:
308            res = self.parent._descriptor.all_relationships
309        else:
310            res = dict()
311        res.update(self.relationships)
312        return res
313    all_relationships = property(all_relationships)
314
315    def setup_relationships(cls):
316        for relationship in list(EntityDescriptor.uninitialized_rels):
317            if relationship.setup():
318                EntityDescriptor.uninitialized_rels.remove(relationship)
319    setup_relationships = classmethod(setup_relationships)
320
321class EntityMeta(type):
322    """
323    Entity meta class.
324    You should only use this if you want to define your own base class for your
325    entities (ie you don't want to use the provided 'Entity' class).
326    """
327
328    def __init__(cls, name, bases, dict_):
329        # only process subclasses of Entity, not Entity itself
330        if bases[0] is object:
331            return
332
333        # create the entity descriptor
334        desc = cls._descriptor = EntityDescriptor(cls)
335        EntityDescriptor.current = desc
336       
337        # process statements
338        Statement.process(cls)
339       
340        # setup misc options here (like tablename etc.)
341        desc.setup_options()
342       
343        # create table & assign (empty) mapper
344        desc.setup()
345
346    def q(cls):
347        return Query(cls, session=elixir.objectstore.session)
348    q = property(q)
349
350class Entity(object):
351    '''
352    The base class for all entities
353   
354    All Elixir model objects should inherit from this class. Statements can
355    appear within the body of the definition of an entity to define its
356    fields, relationships, and other options.
357   
358    Here is an example:
359
360    ::
361   
362        class Person(Entity):
363            has_field('name', Unicode(128))
364            has_field('birthdate', DateTime, default=datetime.now)
365   
366    Please note, that if you don't specify any primary keys, Elixir will
367    automatically create one called ``id``.
368   
369    For further information, please refer to the provided examples or
370    tutorial.
371    '''
372
373    __metaclass__ = EntityMeta
374
375    def __init__(self, **kwargs):
376         for key, value in kwargs.items():
377             setattr(self, key, value)
Note: See TracBrowser for help on using the browser.