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

Revision 64, 9.9 kB (checked in by ged, 6 years ago)

- implemented singletable non-polymorphic inheritance
- added support for the nullable and column_kwargs kwargs on BelongsTo

relationships (forwarded to the SA Column).

- added support for the use_alter and constraint_kwargs kwargs on BelongsTo

relationships (forwarded to SA ForeignKeyConstraint).

-> removed the systematic use_alter on BelongsTo relations since it

can now be specified only when needed.

-> removed it from HasAndBelongsToMany relations, since I think a

circular foreign key dependency can't happen with those relations.

- corrected some docstrings
- added some comments

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