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

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