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

Revision 33, 8.2 kB (checked in by joshua, 6 years ago)

- added "pudge_all" to most files
- added 'license="MIT License"' to setup.py

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