root / elixir / tags / 0.7.0 / elixir / entity.py

Revision 480, 43.2 kB (checked in by ged, 5 months ago)

- Provide our own Session.mapper equivalent to avoid SQLAlchemy 0.5.5+

deprecation warning. This mapper autosave object instances on init unless
save_on_init=False is passed as a mapper argument (closes #92).

- simplified a few tests in test_options.py

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