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

Revision 81, 11.1 kB (checked in by ged, 6 years ago)

- Applied patch from "Wavy" so that columns of a table are in the same order

as they were declared.

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