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

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