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

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