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

Revision 322, 40.6 kB (checked in by ged, 5 years ago)

- Added two new methods on the base entity: from_dict and to_dict, which can

be used to create (or output) a whole hierarchy of instances from (to) a
simple JSON-like dictionary notation (patch from Paul Johnston,
closes ticket #40).

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