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

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