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

Revision 94, 11.5 kB (checked in by ged, 6 years ago)

pep8fix

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