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

Revision 513, 37.9 kB (checked in by ged, 5 years ago)

- do not leak options_defaults from a subclass into a parent class
- do not initialize useless attributes for base and abstract classes

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