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

Revision 96, 11.6 kB (checked in by ged, 6 years ago)

- The tablename option can now be given a callable so that people can provide

their own function to get the table name for an entity. The tablename option
can now also be set globally (using the options_defaults dictionary). Of
course, this only makes sense for the callable usecase.

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