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

Revision 53, 8.3 kB (checked in by ged, 6 years ago)

further reorder things... hopefully fixing the autoload problem

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