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

Revision 74, 11.0 kB (checked in by ged, 6 years ago)

- added support for deferred columns (use the "deferred" keyword argument on

fields)

- added a "required" keyword argument on fields
- renamed the "nullable" keyword argument of BelongsTo relationships to

"required".

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