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

Revision 504, 37.5 kB (checked in by ged, 4 years ago)

- Fixed custom bases classes and versioned extension when used with zope

interfaces (closes #98, patch from Valentin Lab)

Line 
1'''
2This module provides the ``Entity`` base class, as well as its metaclass
3``EntityMeta``.
4'''
5
6import sys
7import types
8import warnings
9
10from copy import copy
11
12import sqlalchemy
13from sqlalchemy import Table, Column, Integer, desc, ForeignKey, and_, \
14                       ForeignKeyConstraint
15from sqlalchemy.orm import MapperExtension, mapper, object_session, \
16                           EXT_CONTINUE, polymorphic_union, ScopedSession, \
17                           ColumnProperty
18from sqlalchemy.sql import ColumnCollection
19
20import elixir
21from elixir.statements import process_mutators
22from elixir import options
23from elixir.properties import Property
24
25DEBUG = False
26
27__doc_all__ = ['Entity', 'EntityMeta']
28
29
30def session_mapper_factory(scoped_session):
31    def session_mapper(cls, *args, **kwargs):
32        if kwargs.pop('save_on_init', True):
33            old_init = cls.__init__
34            def __init__(self, *args, **kwargs):
35                old_init(self, *args, **kwargs)
36                scoped_session.add(self)
37            cls.__init__ = __init__
38        cls.query = scoped_session.query_property()
39        return mapper(cls, *args, **kwargs)
40    return session_mapper
41
42
43class EntityDescriptor(object):
44    '''
45    EntityDescriptor describes fields and options needed for table creation.
46    '''
47
48    def __init__(self, entity):
49        entity.table = None
50        entity.mapper = None
51
52        self.entity = entity
53        # entity.__module__ is not always reliable (eg in mod_python)
54        self.module = sys.modules.get(entity.__module__)
55
56        # used for multi-table inheritance
57        self.join_condition = None
58        self.has_pk = False
59        self._pk_col_done = False
60
61        self.builders = []
62
63        self.parent = None
64        #XXX: use entity.__subclasses__ ?
65        self.children = []
66
67        for base in entity.__bases__:
68            if isinstance(base, EntityMeta):
69                if is_entity(base):
70                    if self.parent:
71                        raise Exception(
72                            '%s entity inherits from several entities, '
73                            'and this is not supported.'
74                            % self.entity.__name__)
75                    else:
76                        self.parent = base
77                        self.base = base._descriptor.base
78                        self.parent._descriptor.children.append(entity)
79                else:
80                    self.base = base
81
82        # columns and constraints waiting for a table to exist
83        self._columns = ColumnCollection()
84        self.constraints = []
85
86        # properties (it is only useful for checking dupe properties at the
87        # moment, and when adding properties before the mapper is created,
88        # which shouldn't happen).
89        self.properties = {}
90
91        #
92        self.relationships = []
93
94        # set default value for options
95        self.table_args = []
96
97        # base class options_defaults
98        base_defaults = getattr(self.base, 'options_defaults', {})
99        complete_defaults = options.options_defaults.copy()
100        complete_defaults.update({
101            'metadata': elixir.metadata,
102            'session': elixir.session,
103            'collection': elixir.entities
104        })
105
106        # set default value for other options
107        for key in options.valid_options:
108            value = base_defaults.get(key, complete_defaults[key])
109            if isinstance(value, dict):
110                value = value.copy()
111            setattr(self, key, value)
112
113        # override options with module-level defaults defined
114        for key in ('metadata', 'session', 'collection'):
115            attr = '__%s__' % key
116            if hasattr(self.module, attr):
117                setattr(self, key, getattr(self.module, attr))
118
119    def setup_options(self):
120        '''
121        Setup any values that might depend on the "using_options" class
122        mutator. For example, the tablename or the metadata.
123        '''
124        elixir.metadatas.add(self.metadata)
125        if self.collection is not None:
126            self.collection.append(self.entity)
127
128        entity = self.entity
129        if self.parent:
130            if self.inheritance == 'single':
131                self.tablename = self.parent._descriptor.tablename
132
133        if not self.tablename:
134            if self.shortnames:
135                self.tablename = entity.__name__.lower()
136            else:
137                modulename = entity.__module__.replace('.', '_')
138                tablename = "%s_%s" % (modulename, entity.__name__)
139                self.tablename = tablename.lower()
140        elif hasattr(self.tablename, '__call__'):
141            self.tablename = self.tablename(entity)
142
143        if not self.identity:
144            if 'polymorphic_identity' in self.mapper_options:
145                self.identity = self.mapper_options['polymorphic_identity']
146            else:
147                #TODO: include module name
148                self.identity = entity.__name__.lower()
149        elif 'polymorphic_identity' in self.mapper_options:
150            raise Exception('You cannot use the "identity" option and the '
151                            'polymorphic_identity mapper option at the same '
152                            'time.')
153        elif hasattr(self.identity, '__call__'):
154            self.identity = self.identity(entity)
155
156        if self.polymorphic:
157            if not isinstance(self.polymorphic, basestring):
158                self.polymorphic = options.DEFAULT_POLYMORPHIC_COL_NAME
159
160    #---------------------
161    # setup phase methods
162
163    def setup_autoload_table(self):
164        self.setup_table(True)
165
166    def create_pk_cols(self):
167        """
168        Create primary_key columns. That is, call the 'create_pk_cols'
169        builders then add a primary key to the table if it hasn't already got
170        one and needs one.
171
172        This method is "semi-recursive" in some cases: it calls the
173        create_keys method on ManyToOne relationships and those in turn call
174        create_pk_cols on their target. It shouldn't be possible to have an
175        infinite loop since a loop of primary_keys is not a valid situation.
176        """
177        if self._pk_col_done:
178            return
179
180        self.call_builders('create_pk_cols')
181
182        if not self.autoload:
183            if self.parent:
184                if self.inheritance == 'multi':
185                    # Add columns with foreign keys to the parent's primary
186                    # key columns
187                    parent_desc = self.parent._descriptor
188                    tablename = parent_desc.table_fullname
189                    join_clauses = []
190                    for pk_col in parent_desc.primary_keys:
191                        colname = options.MULTIINHERITANCECOL_NAMEFORMAT % \
192                                  {'entity': self.parent.__name__.lower(),
193                                   'key': pk_col.key}
194
195                        # It seems like SA ForeignKey is not happy being given
196                        # a real column object when said column is not yet
197                        # attached to a table
198                        pk_col_name = "%s.%s" % (tablename, pk_col.key)
199                        fk = ForeignKey(pk_col_name, ondelete='cascade')
200                        col = Column(colname, pk_col.type, fk,
201                                     primary_key=True)
202                        self.add_column(col)
203                        join_clauses.append(col == pk_col)
204                    self.join_condition = and_(*join_clauses)
205                elif self.inheritance == 'concrete':
206                    # Copy primary key columns from the parent.
207                    for col in self.parent._descriptor.columns:
208                        if col.primary_key:
209                            self.add_column(col.copy())
210            elif not self.has_pk and self.auto_primarykey:
211                if isinstance(self.auto_primarykey, basestring):
212                    colname = self.auto_primarykey
213                else:
214                    colname = options.DEFAULT_AUTO_PRIMARYKEY_NAME
215
216                self.add_column(
217                    Column(colname, options.DEFAULT_AUTO_PRIMARYKEY_TYPE,
218                           primary_key=True))
219        self._pk_col_done = True
220
221    def setup_relkeys(self):
222        self.call_builders('create_non_pk_cols')
223
224    def before_table(self):
225        self.call_builders('before_table')
226
227    def setup_table(self, only_autoloaded=False):
228        '''
229        Create a SQLAlchemy table-object with all columns that have been
230        defined up to this point.
231        '''
232        if self.entity.table is not None:
233            return
234
235        if self.autoload != only_autoloaded:
236            return
237
238        kwargs = self.table_options
239        if self.autoload:
240            args = self.table_args
241            kwargs['autoload'] = True
242        else:
243            if self.parent:
244                if self.inheritance == 'single':
245                    # we know the parent is setup before the child
246                    self.entity.table = self.parent.table
247
248                    # re-add the entity columns to the parent entity so that
249                    # they are added to the parent's table (whether the
250                    # parent's table is already setup or not).
251                    for col in self._columns:
252                        self.parent._descriptor.add_column(col)
253                    for constraint in self.constraints:
254                        self.parent._descriptor.add_constraint(constraint)
255                    return
256                elif self.inheritance == 'concrete':
257                    #TODO: we should also copy columns from the parent table
258                    # if the parent is a base (abstract?) entity (whatever the
259                    # inheritance type -> elif will need to be changed)
260
261                    # Copy all non-primary key columns from parent table
262                    # (primary key columns have already been copied earlier).
263                    for col in self.parent._descriptor.columns:
264                        if not col.primary_key:
265                            self.add_column(col.copy())
266
267                    #FIXME: use the public equivalent of _get_colspec when
268                    # available (e.target_fullname)
269                    for con in self.parent._descriptor.constraints:
270                        self.add_constraint(
271                            ForeignKeyConstraint(
272                                [e.parent.key for e in con.elements],
273                                [e._get_colspec() for e in con.elements],
274                                name=con.name, #TODO: modify it
275                                onupdate=con.onupdate, ondelete=con.ondelete,
276                                use_alter=con.use_alter))
277
278            if self.polymorphic and \
279               self.inheritance in ('single', 'multi') and \
280               self.children and not self.parent:
281                self.add_column(Column(self.polymorphic,
282                                       options.POLYMORPHIC_COL_TYPE))
283
284            if self.version_id_col:
285                if not isinstance(self.version_id_col, basestring):
286                    self.version_id_col = options.DEFAULT_VERSION_ID_COL_NAME
287                self.add_column(Column(self.version_id_col, Integer))
288
289            args = list(self.columns) + self.constraints + self.table_args
290        self.entity.table = Table(self.tablename, self.metadata,
291                                  *args, **kwargs)
292        if DEBUG:
293            print self.entity.table.repr2()
294
295    def setup_reltables(self):
296        self.call_builders('create_tables')
297
298    def after_table(self):
299        self.call_builders('after_table')
300
301    def setup_events(self):
302        def make_proxy_method(methods):
303            def proxy_method(self, mapper, connection, instance):
304                for func in methods:
305                    ret = func(instance)
306                    # I couldn't commit myself to force people to
307                    # systematicaly return EXT_CONTINUE in all their event
308                    # methods.
309                    # But not doing that diverge to how SQLAlchemy works.
310                    # I should try to convince Mike to do EXT_CONTINUE by
311                    # default, and stop processing as the special case.
312#                    if ret != EXT_CONTINUE:
313                    if ret is not None and ret != EXT_CONTINUE:
314                        return ret
315                return EXT_CONTINUE
316            return proxy_method
317
318        # create a list of callbacks for each event
319        methods = {}
320
321        all_methods = getmembers(self.entity,
322                                 lambda a: isinstance(a, types.MethodType))
323
324        for name, method in all_methods:
325            for event in getattr(method, '_elixir_events', []):
326                event_methods = methods.setdefault(event, [])
327                event_methods.append(method)
328
329        if not methods:
330            return
331
332        # transform that list into methods themselves
333        for event in methods:
334            methods[event] = make_proxy_method(methods[event])
335
336        # create a custom mapper extension class, tailored to our entity
337        ext = type('EventMapperExtension', (MapperExtension,), methods)()
338
339        # then, make sure that the entity's mapper has our mapper extension
340        self.add_mapper_extension(ext)
341
342    def before_mapper(self):
343        self.call_builders('before_mapper')
344
345    def _get_children(self):
346        children = self.children[:]
347        for child in self.children:
348            children.extend(child._descriptor._get_children())
349        return children
350
351    def translate_order_by(self, order_by):
352        if isinstance(order_by, basestring):
353            order_by = [order_by]
354
355        order = []
356        for colname in order_by:
357            col = self.get_column(colname.strip('-'))
358            if colname.startswith('-'):
359                col = desc(col)
360            order.append(col)
361        return order
362
363    def setup_mapper(self):
364        '''
365        Initializes and assign a mapper to the entity.
366        At this point the mapper will usually have no property as they are
367        added later.
368        '''
369        if self.entity.mapper:
370            return
371
372        # for now we don't support the "abstract" parent class in a concrete
373        # inheritance scenario as demonstrated in
374        # sqlalchemy/test/orm/inheritance/concrete.py
375        # this should be added along other
376        kwargs = {}
377        if self.order_by:
378            kwargs['order_by'] = self.translate_order_by(self.order_by)
379
380        if self.version_id_col:
381            kwargs['version_id_col'] = self.get_column(self.version_id_col)
382
383        if self.inheritance in ('single', 'concrete', 'multi'):
384            if self.parent and \
385               (self.inheritance != 'concrete' or self.polymorphic):
386                # non-polymorphic concrete doesn't need this
387                kwargs['inherits'] = self.parent.mapper
388
389            if self.inheritance == 'multi' and self.parent:
390                kwargs['inherit_condition'] = self.join_condition
391
392            if self.polymorphic:
393                if self.children:
394                    if self.inheritance == 'concrete':
395                        keys = [(self.identity, self.entity.table)]
396                        keys.extend([(child._descriptor.identity, child.table)
397                                     for child in self._get_children()])
398                        # Having the same alias name for an entity and one of
399                        # its child (which is a parent itself) shouldn't cause
400                        # any problem because the join shouldn't be used at
401                        # the same time. But in reality, some versions of SA
402                        # do misbehave on this. Since it doesn't hurt to have
403                        # different names anyway, here they go.
404                        pjoin = polymorphic_union(
405                                    dict(keys), self.polymorphic,
406                                    'pjoin_%s' % self.identity)
407
408                        kwargs['with_polymorphic'] = ('*', pjoin)
409                        kwargs['polymorphic_on'] = \
410                            getattr(pjoin.c, self.polymorphic)
411                    elif not self.parent:
412                        kwargs['polymorphic_on'] = \
413                            self.get_column(self.polymorphic)
414
415                if self.children or self.parent:
416                    kwargs['polymorphic_identity'] = self.identity
417
418                if self.parent and self.inheritance == 'concrete':
419                    kwargs['concrete'] = True
420
421        if self.parent and self.inheritance == 'single':
422            args = []
423        else:
424            args = [self.entity.table]
425
426        # let user-defined kwargs override Elixir-generated ones, though that's
427        # not very usefull since most of them expect Column instances.
428        kwargs.update(self.mapper_options)
429
430        #TODO: document this!
431        if 'primary_key' in kwargs:
432            cols = self.entity.table.c
433            kwargs['primary_key'] = [getattr(cols, colname) for
434                colname in kwargs['primary_key']]
435
436        # do the mapping
437        if self.session is None:
438            self.entity.mapper = mapper(self.entity, *args, **kwargs)
439        elif isinstance(self.session, ScopedSession):
440            session_mapper = session_mapper_factory(self.session)
441            self.entity.mapper = session_mapper(self.entity, *args, **kwargs)
442        else:
443            raise Exception("Failed to map entity '%s' with its table or "
444                            "selectable. You can only bind an Entity to a "
445                            "ScopedSession object or None for manual session "
446                            "management."
447                            % self.entity.__name__)
448
449    def after_mapper(self):
450        self.call_builders('after_mapper')
451
452    def setup_properties(self):
453        self.call_builders('create_properties')
454
455    def finalize(self):
456        self.call_builders('finalize')
457        self.entity._setup_done = True
458
459    #----------------
460    # helper methods
461
462    def call_builders(self, what):
463        for builder in self.builders:
464            if hasattr(builder, what):
465                getattr(builder, what)()
466
467    def add_column(self, col, check_duplicate=None):
468        '''when check_duplicate is None, the value of the allowcoloverride
469        option of the entity is used.
470        '''
471        if check_duplicate is None:
472            check_duplicate = not self.allowcoloverride
473
474        if col.key in self._columns:
475            if check_duplicate:
476                raise Exception("Column '%s' already exist in '%s' ! " %
477                                (col.key, self.entity.__name__))
478            else:
479                del self._columns[col.key]
480        self._columns.add(col)
481
482        if col.primary_key:
483            self.has_pk = True
484
485        table = self.entity.table
486        if table is not None:
487            if check_duplicate and col.key in table.columns.keys():
488                raise Exception("Column '%s' already exist in table '%s' ! " %
489                                (col.key, table.name))
490            table.append_column(col)
491            if DEBUG:
492                print "table.append_column(%s)" % col
493
494    def add_constraint(self, constraint):
495        self.constraints.append(constraint)
496
497        table = self.entity.table
498        if table is not None:
499            table.append_constraint(constraint)
500
501    def add_property(self, name, property, check_duplicate=True):
502        if check_duplicate and name in self.properties:
503            raise Exception("property '%s' already exist in '%s' ! " %
504                            (name, self.entity.__name__))
505        self.properties[name] = property
506
507#FIXME: something like this is needed to propagate the relationships from
508# parent entities to their children in a concrete inheritance scenario. But
509# this doesn't work because of the backref matching code. In most case
510# (test_concrete.py) it doesn't even happen at all.
511#        if self.children and self.inheritance == 'concrete':
512#            for child in self.children:
513#                child._descriptor.add_property(name, property)
514
515        mapper = self.entity.mapper
516        if mapper:
517            mapper.add_property(name, property)
518            if DEBUG:
519                print "mapper.add_property('%s', %s)" % (name, repr(property))
520
521    def add_mapper_extension(self, extension):
522        extensions = self.mapper_options.get('extension', [])
523        if not isinstance(extensions, list):
524            extensions = [extensions]
525        extensions.append(extension)
526        self.mapper_options['extension'] = extensions
527
528    def get_column(self, key, check_missing=True):
529        #TODO: this needs to work whether the table is already setup or not
530        #TODO: support SA table/autoloaded entity
531        try:
532            return self.columns[key]
533        except KeyError:
534            if check_missing:
535                raise Exception("No column named '%s' found in the table of "
536                                "the '%s' entity!"
537                                % (key, self.entity.__name__))
538
539    def get_inverse_relation(self, rel, check_reverse=True):
540        '''
541        Return the inverse relation of rel, if any, None otherwise.
542        '''
543
544        matching_rel = None
545        for other_rel in self.relationships:
546            if rel.is_inverse(other_rel):
547                if matching_rel is None:
548                    matching_rel = other_rel
549                else:
550                    raise Exception(
551                            "Several relations match as inverse of the '%s' "
552                            "relation in entity '%s'. You should specify "
553                            "inverse relations manually by using the inverse "
554                            "keyword."
555                            % (rel.name, rel.entity.__name__))
556        # When a matching inverse is found, we check that it has only
557        # one relation matching as its own inverse. We don't need the result
558        # of the method though. But we do need to be careful not to start an
559        # infinite recursive loop.
560        if matching_rel and check_reverse:
561            rel.entity._descriptor.get_inverse_relation(matching_rel, False)
562
563        return matching_rel
564
565    def find_relationship(self, name):
566        for rel in self.relationships:
567            if rel.name == name:
568                return rel
569        if self.parent:
570            return self.parent._descriptor.find_relationship(name)
571        else:
572            return None
573
574    #------------------------
575    # some useful properties
576
577    @property
578    def table_fullname(self):
579        '''
580        Complete name of the table for the related entity.
581        Includes the schema name if there is one specified.
582        '''
583        schema = self.table_options.get('schema', None)
584        if schema is not None:
585            return "%s.%s" % (schema, self.tablename)
586        else:
587            return self.tablename
588
589    @property
590    def columns(self):
591        if self.entity.table is not None:
592            return self.entity.table.columns
593        else:
594            #FIXME: depending on the type of inheritance, we should also
595            # return the parent entity's columns (for example for order_by
596            # using a column defined in the parent.
597            return self._columns
598
599    @property
600    def primary_keys(self):
601        """
602        Returns the list of primary key columns of the entity.
603
604        This property isn't valid before the "create_pk_cols" phase.
605        """
606        if self.autoload:
607            return [col for col in self.entity.table.primary_key.columns]
608        else:
609            if self.parent and self.inheritance == 'single':
610                return self.parent._descriptor.primary_keys
611            else:
612                return [col for col in self.columns if col.primary_key]
613
614    @property
615    def table(self):
616        if self.entity.table is not None:
617            return self.entity.table
618        else:
619            return FakeTable(self)
620
621    @property
622    def primary_key_properties(self):
623        """
624        Returns the list of (mapper) properties corresponding to the primary
625        key columns of the table of the entity.
626
627        This property caches its value, so it shouldn't be called before the
628        entity is fully set up.
629        """
630        if not hasattr(self, '_pk_props'):
631            col_to_prop = {}
632            mapper = self.entity.mapper
633            for prop in mapper.iterate_properties:
634                if isinstance(prop, ColumnProperty):
635                    for col in prop.columns:
636                        for col in col.proxy_set:
637                            col_to_prop[col] = prop
638            pk_cols = [c for c in mapper.mapped_table.c if c.primary_key]
639            self._pk_props = [col_to_prop[c] for c in pk_cols]
640        return self._pk_props
641
642class FakePK(object):
643    def __init__(self, descriptor):
644        self.descriptor = descriptor
645
646    def columns(self):
647        return self.descriptor.primary_keys
648    columns = property(columns)
649
650class FakeTable(object):
651    def __init__(self, descriptor):
652        self.descriptor = descriptor
653        self.primary_key = FakePK(descriptor)
654
655    def columns(self):
656        return self.descriptor.columns
657    columns = property(columns)
658
659    def fullname(self):
660        '''
661        Complete name of the table for the related entity.
662        Includes the schema name if there is one specified.
663        '''
664        schema = self.descriptor.table_options.get('schema', None)
665        if schema is not None:
666            return "%s.%s" % (schema, self.descriptor.tablename)
667        else:
668            return self.descriptor.tablename
669    fullname = property(fullname)
670
671
672def is_entity(cls):
673    """
674    Scan the bases classes of `cls` to see if any is an instance of
675    EntityMeta. If we don't find any, it means it is either an unrelated class
676    or an entity base class (like the 'Entity' class).
677    """
678    for base in cls.__bases__:
679        if isinstance(base, EntityMeta):
680            return True
681    return False
682
683
684# Note that we don't use inspect.getmembers because of
685# http://bugs.python.org/issue1785
686# See also http://elixir.ematia.de/trac/changeset/262
687def getmembers(object, predicate=None):
688    base_props = []
689    for key in dir(object):
690        try:
691            value = getattr(object, key)
692        except AttributeError:
693            continue
694        if not predicate or predicate(value):
695            base_props.append((key, value))
696    return base_props
697
698
699def instrument_class(cls):
700    """
701    Instrument a class as an Entity. This is usually done automatically through
702    the EntityMeta metaclass.
703    """
704    # create the entity descriptor
705    desc = cls._descriptor = EntityDescriptor(cls)
706
707    # Determine whether this entity is a *direct* subclass of its base entity
708    entity_base = None
709    for base in cls.__bases__:
710        if isinstance(base, EntityMeta):
711            if not is_entity(base):
712                entity_base = base
713
714    if entity_base:
715        # If so, copy the base entity properties ('Property' instances).
716        # We use inspect.getmembers (instead of __dict__) so that we also
717        # get the properties from the parents of the base_class if any.
718        base_props = getmembers(entity_base,
719                                lambda a: isinstance(a, Property))
720        base_props = [(name, copy(attr)) for name, attr in base_props]
721    else:
722        base_props = []
723
724    # Process attributes (using the assignment syntax), looking for
725    # 'Property' instances and attaching them to this entity.
726    properties = [(name, attr) for name, attr in cls.__dict__.iteritems()
727                               if isinstance(attr, Property)]
728    sorted_props = sorted(base_props + properties,
729                          key=lambda i: i[1]._counter)
730    for name, prop in sorted_props:
731        prop.attach(cls, name)
732
733    # Process mutators
734    process_mutators(cls)
735
736    # setup misc options here (like tablename etc.)
737    desc.setup_options()
738
739
740class EntityMeta(type):
741    """
742    Entity meta class.
743    You should only use it directly if you want to define your own base class
744    for your entities (ie you don't want to use the provided 'Entity' class).
745    """
746
747    def __init__(cls, name, bases, dict_):
748        # Only process further subclasses of the base classes (Entity et al.),
749        # not the base classes themselves. We don't want the base entities to
750        # be registered in an entity collection, nor to have a table name and
751        # so on.
752        if not is_entity(cls):
753            if isinstance(cls, EntityMeta):
754                process_mutators(cls)
755            return
756
757        instrument_class(cls)
758
759    def __setattr__(cls, key, value):
760        if isinstance(value, Property):
761            if hasattr(cls, '_setup_done'):
762                raise Exception('Cannot set attribute on a class after '
763                                'setup_all')
764            else:
765                value.attach(cls, key)
766        else:
767            type.__setattr__(cls, key, value)
768
769
770def setup_entities(entities):
771    '''Setup all entities in the list passed as argument'''
772
773    for entity in entities:
774        # delete all Elixir properties so that it doesn't interfere with
775        # SQLAlchemy. At this point they should have be converted to
776        # builders.
777        for name, attr in entity.__dict__.items():
778            if isinstance(attr, Property):
779                delattr(entity, name)
780
781    for method_name in (
782            'setup_autoload_table', 'create_pk_cols', 'setup_relkeys',
783            'before_table', 'setup_table', 'setup_reltables', 'after_table',
784            'setup_events',
785            'before_mapper', 'setup_mapper', 'after_mapper',
786            'setup_properties',
787            'finalize'):
788#        if DEBUG:
789#            print "=" * 40
790#            print method_name
791#            print "=" * 40
792        for entity in entities:
793#            print entity.__name__, "...",
794            if hasattr(entity, '_setup_done'):
795#                print "already done"
796                continue
797            method = getattr(entity._descriptor, method_name)
798            method()
799#            print "ok"
800
801
802def cleanup_entities(entities):
803    """
804    Try to revert back the list of entities passed as argument to the state
805    they had just before their setup phase.
806
807    As of now, this function is *not* functional in that it doesn't revert to
808    the exact same state the entities were before setup. For example, the
809    properties do not work yet as those would need to be regenerated (since the
810    columns they are based on are regenerated too -- and as such the
811    corresponding joins are not correct) but this doesn't happen because of
812    the way relationship setup is designed to be called only once (especially
813    the backref stuff in create_properties).
814    """
815    for entity in entities:
816        desc = entity._descriptor
817
818        if hasattr(entity, '_setup_done'):
819            del entity._setup_done
820
821        entity.table = None
822        entity.mapper = None
823
824        desc._pk_col_done = False
825        desc.has_pk = False
826        desc._columns = ColumnCollection()
827        desc.constraints = []
828        desc.properties = {}
829
830class EntityBase(object):
831    """
832    This class holds all methods of the "Entity" base class, but does not act
833    as a base class itself (it does not use the EntityMeta metaclass), but
834    rather as a parent class for Entity. This is meant so that people who want
835    to provide their own base class but don't want to loose or copy-paste all
836    the methods of Entity can do so by inheriting from EntityBase:
837
838    .. sourcecode:: python
839
840        class MyBase(EntityBase):
841            __metaclass__ = EntityMeta
842
843            def myCustomMethod(self):
844                # do something great
845    """
846
847    def __init__(self, **kwargs):
848        self.set(**kwargs)
849
850    def set(self, **kwargs):
851        for key, value in kwargs.iteritems():
852            setattr(self, key, value)
853
854    @classmethod
855    def update_or_create(cls, data, surrogate=True):
856        pk_props = cls._descriptor.primary_key_properties
857
858        # if all pk are present and not None
859        if not [1 for p in pk_props if data.get(p.key) is None]:
860            pk_tuple = tuple([data[prop.key] for prop in pk_props])
861            record = cls.query.get(pk_tuple)
862            if record is None:
863                if surrogate:
864                    raise Exception("cannot create surrogate with pk")
865                else:
866                    record = cls()
867        else:
868            if surrogate:
869                record = cls()
870            else:
871                raise Exception("cannot create non surrogate without pk")
872        record.from_dict(data)
873        return record
874
875    def from_dict(self, data):
876        """
877        Update a mapped class with data from a JSON-style nested dict/list
878        structure.
879        """
880        # surrogate can be guessed from autoincrement/sequence but I guess
881        # that's not 100% reliable, so we'll need an override
882
883        mapper = sqlalchemy.orm.object_mapper(self)
884
885        for key, value in data.iteritems():
886            if isinstance(value, dict):
887                dbvalue = getattr(self, key)
888                rel_class = mapper.get_property(key).mapper.class_
889                pk_props = rel_class._descriptor.primary_key_properties
890
891                # If the data doesn't contain any pk, and the relationship
892                # already has a value, update that record.
893                if not [1 for p in pk_props if p.key in data] and \
894                   dbvalue is not None:
895                    dbvalue.from_dict(value)
896                else:
897                    record = rel_class.update_or_create(value)
898                    setattr(self, key, record)
899            elif isinstance(value, list) and \
900                 value and isinstance(value[0], dict):
901
902                rel_class = mapper.get_property(key).mapper.class_
903                new_attr_value = []
904                for row in value:
905                    if not isinstance(row, dict):
906                        raise Exception(
907                                'Cannot send mixed (dict/non dict) data '
908                                'to list relationships in from_dict data.')
909                    record = rel_class.update_or_create(row)
910                    new_attr_value.append(record)
911                setattr(self, key, new_attr_value)
912            else:
913                setattr(self, key, value)
914
915    def to_dict(self, deep={}, exclude=[]):
916        """Generate a JSON-style nested dict/list structure from an object."""
917        col_prop_names = [p.key for p in self.mapper.iterate_properties \
918                                      if isinstance(p, ColumnProperty)]
919        data = dict([(name, getattr(self, name))
920                     for name in col_prop_names if name not in exclude])
921        for rname, rdeep in deep.iteritems():
922            dbdata = getattr(self, rname)
923            #FIXME: use attribute names (ie coltoprop) instead of column names
924            fks = self.mapper.get_property(rname).remote_side
925            exclude = [c.name for c in fks]
926            if dbdata is None:
927                data[rname] = None
928            elif isinstance(dbdata, list):
929                data[rname] = [o.to_dict(rdeep, exclude) for o in dbdata]
930            else:
931                data[rname] = dbdata.to_dict(rdeep, exclude)
932        return data
933
934    # session methods
935    def flush(self, *args, **kwargs):
936        return object_session(self).flush([self], *args, **kwargs)
937
938    def delete(self, *args, **kwargs):
939        return object_session(self).delete(self, *args, **kwargs)
940
941    def expire(self, *args, **kwargs):
942        return object_session(self).expire(self, *args, **kwargs)
943
944    def refresh(self, *args, **kwargs):
945        return object_session(self).refresh(self, *args, **kwargs)
946
947    def expunge(self, *args, **kwargs):
948        return object_session(self).expunge(self, *args, **kwargs)
949
950    # This bunch of session methods, along with all the query methods below
951    # only make sense when using a global/scoped/contextual session.
952    @property
953    def _global_session(self):
954        return self._descriptor.session.registry()
955
956    def merge(self, *args, **kwargs):
957        return self._global_session.merge(self, *args, **kwargs)
958
959    def save(self, *args, **kwargs):
960        return self._global_session.save(self, *args, **kwargs)
961
962    def update(self, *args, **kwargs):
963        return self._global_session.update(self, *args, **kwargs)
964
965    # only exist in SA < 0.5
966    # IMO, the replacement (session.add) doesn't sound good enough to be added
967    # here. For example: "o = Order(); o.add()" is not very telling. It's
968    # better to leave it as "session.add(o)"
969    def save_or_update(self, *args, **kwargs):
970        return self._global_session.save_or_update(self, *args, **kwargs)
971
972    # query methods
973    @classmethod
974    def get_by(cls, *args, **kwargs):
975        """
976        Returns the first instance of this class matching the given criteria.
977        This is equivalent to:
978        session.query(MyClass).filter_by(...).first()
979        """
980        return cls.query.filter_by(*args, **kwargs).first()
981
982    @classmethod
983    def get(cls, *args, **kwargs):
984        """
985        Return the instance of this class based on the given identifier,
986        or None if not found. This is equivalent to:
987        session.query(MyClass).get(...)
988        """
989        return cls.query.get(*args, **kwargs)
990
991
992class Entity(EntityBase):
993    '''
994    The base class for all entities
995
996    All Elixir model objects should inherit from this class. Statements can
997    appear within the body of the definition of an entity to define its
998    fields, relationships, and other options.
999
1000    Here is an example:
1001
1002    .. sourcecode:: python
1003
1004        class Person(Entity):
1005            name = Field(Unicode(128))
1006            birthdate = Field(DateTime, default=datetime.now)
1007
1008    Please note, that if you don't specify any primary keys, Elixir will
1009    automatically create one called ``id``.
1010
1011    For further information, please refer to the provided examples or
1012    tutorial.
1013    '''
1014    __metaclass__ = EntityMeta
1015
Note: See TracBrowser for help on using the browser.