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

Revision 170, 14.4 kB (checked in by cleverdevil, 7 years ago)

First part of moving events into the core of elixir. Next, I will be moving events.py out of ext/ and directly into the elixir namespace.

Line 
1'''
2Entity baseclass, metaclass and descriptor
3'''
4
5from sqlalchemy                     import Table, Integer, desc
6from sqlalchemy.orm                 import deferred, Query, MapperExtension
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
20import inspect
21
22__pudge_all__ = ['Entity', 'EntityMeta']
23
24DEFAULT_AUTO_PRIMARYKEY_NAME = "id"
25DEFAULT_AUTO_PRIMARYKEY_TYPE = Integer
26DEFAULT_VERSION_ID_COL = "row_version"
27
28class EntityDescriptor(object):
29    '''
30    EntityDescriptor describes fields and options needed for table creation.
31    '''
32   
33    uninitialized_rels = set()
34    current = None
35   
36    def __init__(self, entity):
37        entity.table = None
38        entity.mapper = None
39
40        self.entity = entity
41        self.module = sys.modules[entity.__module__]
42
43        self.has_pk = False
44
45        self.parent = None
46        for base in entity.__bases__:
47            if issubclass(base, Entity) and base is not Entity:
48                if self.parent:
49                    raise Exception('%s entity inherits from several entities,'
50                                    ' and this is not supported.' 
51                                    % self.entity.__name__)
52                else:
53                    self.parent = base
54
55        self.fields = OrderedDict()
56        self.relationships = dict()
57        self.delayed_properties = dict()
58        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)
64
65        # set default value for options
66        self.order_by = None
67        self.table_args = list()
68        self.metadata = getattr(self.module, 'metadata', elixir.metadata)
69
70        for option in ('inheritance', 
71                       'autoload', 'tablename', 'shortnames', 
72                       'auto_primarykey',
73                       'version_id_col'):
74            setattr(self, option, options_defaults[option])
75
76        for option_dict in ('mapper_options', 'table_options'):
77            setattr(self, option_dict, options_defaults[option_dict].copy())
78   
79    def setup_options(self):
80        '''
81        Setup any values that might depend on using_options. For example, the
82        tablename or the metadata.
83        '''
84        elixir.metadatas.add(self.metadata)
85
86        entity = self.entity
87
88        if not self.tablename:
89            if self.shortnames:
90                self.tablename = entity.__name__.lower()
91            else:
92                modulename = entity.__module__.replace('.', '_')
93                tablename = "%s_%s" % (modulename, entity.__name__)
94                self.tablename = tablename.lower()
95        elif callable(self.tablename):
96            self.tablename = self.tablename(entity)
97   
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   
125    def setup_events(self):
126        # create a list of callbacks for each event
127        methods = {}
128        for name, func in inspect.getmembers(self.entity, inspect.ismethod):
129            if hasattr(func, '_elixir_events'):
130                for event in func._elixir_events:
131                    event_methods = methods.setdefault(event, [])
132                    event_methods.append(func)
133       
134        if not methods:
135            return
136       
137        # transform that list into methods themselves
138        for event in methods:
139            methods[event] = self.make_proxy_method(methods[event])
140       
141        # create a custom mapper extension class, tailored to our entity
142        ext = type('EventMapperExtension', (MapperExtension,), methods)()
143       
144        # then, make sure that the entity's mapper has our mapper extension
145        self.add_mapper_extension(ext)
146   
147    def make_proxy_method(self, methods):
148        def proxy_method(self, mapper, connection, instance):
149            for func in methods:
150                func(instance)
151        return proxy_method
152   
153    def translate_order_by(self, order_by):
154        if isinstance(order_by, basestring):
155            order_by = [order_by]
156       
157        order = list()
158        for field in order_by:
159            col = self.fields[field.strip('-')].column
160            if field.startswith('-'):
161                col = desc(col)
162            order.append(col)
163        return order
164
165    def setup_mapper(self):
166        '''
167        Initializes and assign an (empty!) mapper to the entity.
168        '''
169        if self.entity.mapper:
170            return
171       
172        session = getattr(self.module, 'session', elixir.objectstore)
173       
174        kwargs = self.mapper_options
175        if self.order_by:
176            kwargs['order_by'] = self.translate_order_by(self.order_by)
177       
178        if self.version_id_col:
179            kwargs['version_id_col'] = self.fields[self.version_id_col].column
180
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()
188                kwargs['inherits'] = self.parent.mapper
189
190        properties = dict()
191        for field in self.fields.itervalues():
192            if field.deferred:
193                group = None
194                if isinstance(field.deferred, basestring):
195                    group = field.deferred
196                properties[field.column.name] = deferred(field.column,
197                                                         group=group)
198
199        #TODO: make this happen after the rel columns have been added.
200        for name, prop in self.delayed_properties.iteritems():
201            properties[name] = self.evaluate_property(prop)
202        self.delayed_properties.clear()
203
204        if 'primary_key' in kwargs:
205            cols = self.entity.table.c
206            kwargs['primary_key'] = [getattr(cols, colname) for
207                colname in kwargs['primary_key']]
208
209        assign_mapper(session.context, self.entity, self.entity.table,
210                      properties=properties, **kwargs)
211
212    def evaluate_property(self, prop):
213        if callable(prop):
214            return prop(self.entity.table.c)
215        else:
216            return prop
217
218    def add_property(self, name, prop):
219        if self.entity.mapper:
220            prop_value = self.evaluate_property(prop)
221            self.entity.mapper.add_property(name, prop_value)
222        else:
223            self.delayed_properties[name] = prop
224   
225    def add_mapper_extension(self, extension):
226        extensions = self.mapper_options.get('extension', [])
227        if not isinstance(extensions, list):
228            extensions = [extensions]
229        extensions.append(extension)
230        self.mapper_options['extension'] = extensions
231   
232    def setup_table(self):
233        '''
234        Create a SQLAlchemy table-object with all columns that have been
235        defined up to this point.
236        '''
237        if self.entity.table:
238            return
239       
240        if self.parent:
241            if self.inheritance == 'single':
242                # reuse the parent's table
243                if not self.parent.table:
244                    self.parent._descriptor.setup_table()
245                   
246                self.entity.table = self.parent.table 
247
248                # re-add the entity fields to the parent entity so that they
249                # are added to the parent's table (whether the parent's table
250                # is already setup or not).
251                for field in self.fields.itervalues():
252                    self.parent._descriptor.add_field(field)
253
254                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)
261
262        if self.version_id_col:
263            if not isinstance(self.version_id_col, basestring):
264                self.version_id_col = DEFAULT_VERSION_ID_COL
265            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()
270
271        # create list of columns and constraints
272        args = [field.column for field in self.fields.itervalues()] \
273                    + self.constraints + self.table_args
274       
275        # specify options
276        kwargs = self.table_options
277
278        if self.autoload:
279            kwargs['autoload'] = True
280       
281        self.entity.table = Table(self.tablename, self.metadata, 
282                                  *args, **kwargs)
283
284
285    def create_auto_primary_key(self):
286        '''
287        Creates a primary key
288        '''
289       
290        assert not self.has_pk and self.auto_primarykey
291       
292        if isinstance(self.auto_primarykey, basestring):
293            colname = self.auto_primarykey
294        else:
295            colname = DEFAULT_AUTO_PRIMARYKEY_NAME
296       
297        self.add_field(Field(DEFAULT_AUTO_PRIMARYKEY_TYPE,
298                             colname=colname, primary_key=True))
299       
300    def add_field(self, field):
301        self.fields[field.colname] = field
302       
303        if field.primary_key:
304            self.has_pk = True
305
306        table = self.entity.table
307        if table:
308            table.append_column(field.column)
309   
310    def add_constraint(self, constraint):
311        self.constraints.append(constraint)
312       
313        table = self.entity.table
314        if table:
315            table.append_constraint(constraint)
316       
317    def get_inverse_relation(self, rel, reverse=False):
318        '''
319        Return the inverse relation of rel, if any, None otherwise.
320        '''
321
322        matching_rel = None
323        for other_rel in self.relationships.itervalues():
324            if other_rel.is_inverse(rel):
325                if matching_rel is None:
326                    matching_rel = other_rel
327                else:
328                    raise Exception(
329                            "Several relations match as inverse of the '%s' "
330                            "relation in entity '%s'. You should specify "
331                            "inverse relations manually by using the inverse "
332                            "keyword."
333                            % (rel.name, rel.entity.__name__))
334        # When a matching inverse is found, we check that it has only
335        # one relation matching as its own inverse. We don't need the result
336        # of the method though. But we do need to be careful not to start an
337        # infinite recursive loop.
338        if matching_rel and not reverse:
339            rel.entity._descriptor.get_inverse_relation(matching_rel, True)
340
341        return matching_rel
342
343    def primary_keys(self):
344        return [col for col in self.entity.table.primary_key.columns]
345    primary_keys = property(primary_keys)
346
347    def all_relationships(self):
348        if self.parent:
349            res = self.parent._descriptor.all_relationships
350        else:
351            res = dict()
352        res.update(self.relationships)
353        return res
354    all_relationships = property(all_relationships)
355
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)
361
362class EntityMeta(type):
363    """
364    Entity meta class.
365    You should only use this if you want to define your own base class for your
366    entities (ie you don't want to use the provided 'Entity' class).
367    """
368
369    def __init__(cls, name, bases, dict_):
370        # only process subclasses of Entity, not Entity itself
371        if bases[0] is object:
372            return
373
374        # create the entity descriptor
375        desc = cls._descriptor = EntityDescriptor(cls)
376        EntityDescriptor.current = desc
377
378        # process statements
379        Statement.process(cls)
380
381        # setup misc options here (like tablename etc.)
382        desc.setup_options()
383
384        # create table & assign (empty) mapper
385        desc.setup()
386
387    def q(cls):
388        return Query(cls, session=elixir.objectstore.session)
389    q = property(q)
390
391
392class Entity(object):
393    '''
394    The base class for all entities
395   
396    All Elixir model objects should inherit from this class. Statements can
397    appear within the body of the definition of an entity to define its
398    fields, relationships, and other options.
399   
400    Here is an example:
401
402    ::
403   
404        class Person(Entity):
405            has_field('name', Unicode(128))
406            has_field('birthdate', DateTime, default=datetime.now)
407   
408    Please note, that if you don't specify any primary keys, Elixir will
409    automatically create one called ``id``.
410   
411    For further information, please refer to the provided examples or
412    tutorial.
413    '''
414
415    __metaclass__ = EntityMeta
416
417    def __init__(self, **kwargs):
418        for key, value in kwargs.items():
419            setattr(self, key, value)
Note: See TracBrowser for help on using the browser.