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

Revision 22, 8.0 kB (checked in by ged, 6 years ago)

* implemented order_by translation on relations (has_many and
has_and_belongs_to_many)
* added unit test to demonstrate it, and moved the order_by test there too.
* test_options is now empty.
* some minor cleanups (mainly docstrings adjustment to 79 chars max

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