]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Renamed on_reconstitute to @reconstructor and reconstruct_instance
authorJason Kirtland <jek@discorporate.us>
Fri, 15 Aug 2008 22:03:42 +0000 (22:03 +0000)
committerJason Kirtland <jek@discorporate.us>
Fri, 15 Aug 2008 22:03:42 +0000 (22:03 +0000)
- Moved @reconstructor hooking to mapper
- Expanded reconstructor tests, docs

CHANGES
doc/build/content/mappers.txt
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/orm/mapper.py
test/orm/extendedattr.py
test/orm/mapper.py

diff --git a/CHANGES b/CHANGES
index 029728ca565859117fbca3f176a7dc4336e9b4e2..3bfbfe88e3f90f00795a2fa8875512002963c9c2 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -34,7 +34,11 @@ CHANGES
       joined-table inheritance subclasses, using explicit join
       criteria (i.e. not on a relation).
 
-    - Fixed @on_reconsitute hook for subclasses which inherit from a
+    - @orm.attributes.on_reconsitute and
+      MapperExtension.on_reconstitute have been renamed to
+      @orm.reconstructor and MapperExtension.reconstruct_instance
+
+    - Fixed @reconstructor hook for subclasses which inherit from a
       base class.  [ticket:1129]
 
     - The composite() property type now supports a
index f0821e6e249cf7891465d5261a140b71f8d0ddf9..11c0382523807f4a91c953726dc837c6d6c418d0 100644 (file)
@@ -720,27 +720,62 @@ The "non primary mapper" is a rarely needed feature of SQLAlchemy; in most cases
 
 Versions of SQLAlchemy previous to 0.5 included another mapper flag called "entity_name", as of version 0.5.0 this feature has been removed (it never worked very well).
 
-#### Performing Initialization When an Object Is Loaded {@name=onreconstitute}
+#### Constructors and Object Initialization {@name=reconstructor}
 
-While a mapped object's `__init__()` method works as always during object construction, it's not called when instances of the object are re-created from the database.  This is so that the `__init__()` method can be constructed in any desired way without SQLA requiring any sort of behavior, and also so that an object can control the way it's initialized when constructed new versus reconstituted.
+Mapping imposes no restrictions or requirements on the constructor
+(`__init__`) method for the class.  You are free to require any
+arguments for the function that you wish, assign attributes to the
+instance that are unknown to the ORM, and generally do anything else
+you would normally do when writing a constructor for a Python class.
 
-To support the common use case of instance management which occurs during load, SQLA 0.5 supports this most easily using the `@on_reconstitute` decorator, which is a shortcut to the `MapperExtension.on_reconstitute` method:
+The SQLAlchemy ORM does not call `__init__` when recreating objects
+from database rows.  The ORM's process is somewhat akin to the Python
+standard library's `pickle` module, invoking the low level `__new__`
+method and then quietly restoring attributes directly on the instance
+rather than calling `__init__`.
+
+If you need to do some setup on database-loaded instances before
+they're ready to use, you can use the `@reconstructor` decorator to
+tag a method as the ORM counterpart to `__init__`.  SQLAlchemy will
+call this method with no arguments every time it loads or reconstructs
+one of your instances.  This is useful for recreating transient
+properties that are normally assigned in your `__init__`.
 
     {python}
-    from sqlalchemy.orm.attributes import on_reconstitute
-    
+    from sqlalchemy import orm
+
     class MyMappedClass(object):
         def __init__(self, data):
             self.data = data
-            self.description = "The data is: " + data
-            
-        @on_reconstitute
-        def init_on_load(self):
-            self.description = "The data is: " + self.data
-
-Above, when `MyMappedClass` is constructed, `__init__()` is called with the requirement that the `data` argument is passed, but when loaded during a `Query` operation, `init_on_load()` is called instead.   This method is called after the object's row has been loaded, so scalar attributes will be present, such as above where the `self.data` is available.  Eagerly-loaded collections are generally not available at this stage and will usually only contain the first element.   Any state changes to objects at this stage will not be recorded for the next flush() operation, so the activity within a reconstitute hook should be conservative.
+            # we need stuff on all instances, but not in the database.
+            self.stuff = []
 
-The non-declarative form of `@on_reconsitute` is to use the `on_reconstitute` method of `MapperExtension`, the ORM's mapper-level extension API which is described in the next section.
+        @orm.reconstructor
+        def init_on_load(self):
+            self.stuff = []
+
+When `obj = MyMappedClass()` is executed, Python calls the `__init__`
+method as normal and the `data` argument is required.  When instances
+are loaded during a `Query` operation as in
+`query(MyMappedClass).one()`, `init_on_load` is called instead.
+
+Any method may be tagged as the `reconstructor`, even the `__init__`
+method.  SQLAlchemy will call the reconstructor method with no
+arguments.  Scalar (non-collection) database-mapped attributes of the
+instance will be available for use within the function.
+Eagerly-loaded collections are generally not yet available and will
+usually only contain the first element.  ORM state changes made to
+objects at this stage will not be recorded for the next flush()
+operation, so the activity within a reconstructor should be
+conservative.
+
+While the ORM does not call your `__init__` method, it will modify the
+class's `__init__` slightly.  The method is lightly wrapped to act as
+a trigger for the ORM, allowing mappers to be compiled automatically
+and will fire a `init_instance` event that `MapperExtension`s may
+listen for.  `MapperExtension`s can also listen for a
+`reconstruct_instance` event, analagous to the `reconstructor`
+decorator above.
 
 #### Extending Mapper {@name=extending}
 
index 425a41b3719a562b665304c450922c12563a22fc..e405d76a2756ae171204b939e819130199c324f0 100644 (file)
@@ -44,6 +44,7 @@ from sqlalchemy.orm.properties import (
      SynonymProperty,
      )
 from sqlalchemy.orm import mapper as mapperlib
+from sqlalchemy.orm.mapper import reconstructor
 from sqlalchemy.orm import strategies
 from sqlalchemy.orm.query import AliasOption, Query
 from sqlalchemy.sql import util as sql_util
@@ -83,6 +84,7 @@ __all__ = (
     'object_mapper',
     'object_session',
     'polymorphic_union',
+    'reconstructor',
     'relation',
     'scoped_session',
     'sessionmaker',
index 40878be764f4260b8d00b69cd218074292e83bc8..9ebe9f79f2273f6fdc1a57939ad6dce5dd56dbcd 100644 (file)
@@ -3,14 +3,10 @@
 #
 # This module is part of SQLAlchemy and is released under
 # the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""
-
-Defines SQLAlchemy's system of class instrumentation.
+"""Defines SQLAlchemy's system of class instrumentation..
 
-This module is usually not visible to user applications, but forms
-a large part of the ORM's interactivity.  The primary "public"
-function is the ``on_reconstitute`` decorator which is described in
-the main mapper documentation.
+This module is usually not directly visible to user applications, but
+defines a large part of the ORM's interactivity.
 
 SQLA's instrumentation system is completely customizable, in which
 case an understanding of the general mechanics of this module is helpful.
@@ -26,7 +22,6 @@ from sqlalchemy import util
 from sqlalchemy.util import EMPTY_SET
 from sqlalchemy.orm import interfaces, collections, exc
 import sqlalchemy.exceptions as sa_exc
-import types
 
 # lazy imports
 _entity_info = None
@@ -1056,10 +1051,6 @@ class ClassManager(dict):
         self._instantiable = False
         self.events = self.event_registry_factory()
 
-        for key, meth in util.iterate_attributes(class_):
-            if isinstance(meth, types.FunctionType) and hasattr(meth, '__sa_reconstitute__'):
-                self.events.add_listener('on_load', meth)
-
     def instantiable(self, boolean):
         # experiment, probably won't stay in this form
         assert boolean ^ self._instantiable, (boolean, self._instantiable)
@@ -1465,19 +1456,6 @@ def del_attribute(instance, key):
 def is_instrumented(instance, key):
     return manager_of_class(instance.__class__).is_instrumented(key, search=True)
 
-def on_reconstitute(fn):
-    """Decorate a method as the 'reconstitute' hook.
-    
-    This method will be called based on the 'on_load' event hook.
-    
-    Note that when using ORM mappers, this method is equivalent
-    to MapperExtension.on_reconstitute().
-
-    """
-    fn.__sa_reconstitute__ = True
-    return fn
-    
-    
 class InstrumentationRegistry(object):
     """Private instrumentation registration singleton."""
 
index 283bc10a5b6b9de12fdbd88e4b1f9f3ab829b7bd..0b60483a33d114730fa389d660b18d3dfcce0da0 100644 (file)
@@ -76,10 +76,10 @@ class MapperExtension(object):
         """Perform pre-processing on the given result row and return a
         new row instance.
 
-        This is called when the mapper first receives a row, before 
+        This is called when the mapper first receives a row, before
         the object identity or the instance itself has been derived
         from that row.
-        
+
         """
         return EXT_CONTINUE
 
@@ -143,41 +143,40 @@ class MapperExtension(object):
     def populate_instance(self, mapper, selectcontext, row, instance, **flags):
         """Receive an instance before that instance has
         its attributes populated.
-        
+
         This usually corresponds to a newly loaded instance but may
         also correspond to an already-loaded instance which has
-        unloaded attributes to be populated.  The method may be 
-        called many times for a single instance, as multiple
-        result rows are used to populate eagerly loaded collections.
-
-        If this method returns EXT_CONTINUE, instance
-        population will proceed normally.  If any other value or None
-        is returned, instance population will not proceed, giving this
-        extension an opportunity to populate the instance itself, if
-        desired.
-        
-        As of 0.5, most usages of this hook are obsolete.  
-        For a generic "object has been newly created from a row" hook, 
-        use ``on_reconstitute()``, or the @attributes.on_reconstitute 
+        unloaded attributes to be populated.  The method may be called
+        many times for a single instance, as multiple result rows are
+        used to populate eagerly loaded collections.
+
+        If this method returns EXT_CONTINUE, instance population will
+        proceed normally.  If any other value or None is returned,
+        instance population will not proceed, giving this extension an
+        opportunity to populate the instance itself, if desired.
+
+        As of 0.5, most usages of this hook are obsolete.  For a
+        generic "object has been newly created from a row" hook, use
+        ``reconstruct_instance()``, or the ``@orm.reconstructor``
         decorator.
-        
+
         """
         return EXT_CONTINUE
 
-    def on_reconstitute(self, mapper, instance):
-        """Receive an object instance after it has been created via 
-        ``__new__()``, and after initial attribute population has
-        occurred.  
-        
-        This typicically occurs when the instance is created based 
-        on incoming result rows, and is only called once for that
+    def reconstruct_instance(self, mapper, instance):
+        """Receive an object instance after it has been created via
+        ``__new__``, and after initial attribute population has
+        occurred.
+
+        This typicically occurs when the instance is created based on
+        incoming result rows, and is only called once for that
         instance's lifetime.
-        
+
         Note that during a result-row load, this method is called upon
-        the first row received for this instance; therefore, if eager loaders
-        are to further populate collections on the instance, those will
-        *not* have been completely loaded as of yet.
-        
+        the first row received for this instance. If eager loaders are
+        set to further populate collections on the instance, those
+        will *not* yet be completely loaded.
+
         """
         return EXT_CONTINUE
 
@@ -188,11 +187,12 @@ class MapperExtension(object):
         This is a good place to set up primary key values and such
         that aren't handled otherwise.
 
-        Column-based attributes can be modified within this method which will
-        result in the new value being inserted.  However *no* changes to the overall
-        flush plan can be made; this means any collection modification or
-        save() operations which occur within this method will not take effect
-        until the next flush call.
+        Column-based attributes can be modified within this method
+        which will result in the new value being inserted.  However
+        *no* changes to the overall flush plan can be made; this means
+        any collection modification or save() operations which occur
+        within this method will not take effect until the next flush
+        call.
 
         """
 
@@ -432,15 +432,15 @@ class MapperProperty(object):
 
 class PropComparator(expression.ColumnOperators):
     """defines comparison operations for MapperProperty objects.
-    
+
     PropComparator instances should also define an accessor 'property'
     which returns the MapperProperty associated with this
     PropComparator.
     """
-    
+
     def __clause_element__(self):
         raise NotImplementedError("%r" % self)
-        
+
     def contains_op(a, b):
         return a.contains(b)
     contains_op = staticmethod(contains_op)
@@ -456,30 +456,30 @@ class PropComparator(expression.ColumnOperators):
     def __init__(self, prop, mapper):
         self.prop = self.property = prop
         self.mapper = mapper
-        
+
     def of_type_op(a, class_):
         return a.of_type(class_)
     of_type_op = staticmethod(of_type_op)
-    
+
     def of_type(self, class_):
         """Redefine this object in terms of a polymorphic subclass.
-        
+
         Returns a new PropComparator from which further criterion can be evaluated.
 
         e.g.::
-        
+
             query.join(Company.employees.of_type(Engineer)).\\
                filter(Engineer.name=='foo')
-              
+
         \class_
             a class or mapper indicating that criterion will be against
             this specific subclass.
 
-         
+
         """
-        
+
         return self.operate(PropComparator.of_type_op, class_)
-        
+
     def contains(self, other):
         """Return true if this collection contains other"""
         return self.operate(PropComparator.contains_op, other)
@@ -531,18 +531,18 @@ class StrategizedProperty(MapperProperty):
                 return self.__init_strategy(cls)
         else:
             return self.strategy
-    
+
     def _get_strategy(self, cls):
         try:
             return self.__all_strategies[cls]
         except KeyError:
             return self.__init_strategy(cls)
-    
+
     def __init_strategy(self, cls):
         self.__all_strategies[cls] = strategy = cls(self)
         strategy.init()
         return strategy
-        
+
     def setup(self, context, entity, path, adapter, **kwargs):
         self.__get_context_strategy(context, path + (self.key,)).setup_query(context, entity, path, adapter, **kwargs)
 
@@ -631,10 +631,10 @@ class PropertyOption(MapperOption):
 
     def process_query_property(self, query, paths):
         pass
-    
+
     def __find_entity(self, query, mapper, raiseerr):
         from sqlalchemy.orm.util import _class_to_mapper, _is_aliased_class
-        
+
         if _is_aliased_class(mapper):
             searchfor = mapper
         else:
@@ -648,19 +648,19 @@ class PropertyOption(MapperOption):
                 raise sa_exc.ArgumentError("Can't find entity %s in Query.  Current list: %r" % (searchfor, [str(m.path_entity) for m in query._entities]))
             else:
                 return None
-            
+
     def __get_paths(self, query, raiseerr):
         path = None
         entity = None
         l = []
-        
+
         current_path = list(query._current_path)
-        
+
         if self.mapper:
             entity = self.__find_entity(query, self.mapper, raiseerr)
             mapper = entity.mapper
             path_element = entity.path_entity
-            
+
         for key in util.to_list(self.key):
             if isinstance(key, basestring):
                 tokens = key.split('.')
@@ -684,11 +684,11 @@ class PropertyOption(MapperOption):
                     key = prop.key
                 else:
                     raise sa_exc.ArgumentError("mapper option expects string key or list of attributes")
-            
+
                 if current_path and key == current_path[1]:
                     current_path = current_path[2:]
                     continue
-                
+
                 if prop is None:
                     return []
 
@@ -700,7 +700,7 @@ class PropertyOption(MapperOption):
                     path_element = mapper = getattr(prop, 'mapper', None)
                 if path_element:
                     path_element = path_element.base_mapper
-            
+
         return l
 
 PropertyOption.logger = log.class_logger(PropertyOption)
index 52acdcb339925382dd70d56e57d7139b3b621c51..ae356126e0badd9254afbcce8577d39ed551560f 100644 (file)
@@ -14,6 +14,7 @@ available in [sqlalchemy.orm#].
 
 """
 
+import types
 import weakref
 from itertools import chain
 
@@ -106,15 +107,15 @@ class Mapper(object):
 
         self.class_ = class_
         self.class_manager = None
-            
+
         self.primary_key_argument = primary_key
         self.non_primary = non_primary
-        
+
         if order_by:
             self.order_by = util.to_list(order_by)
         else:
             self.order_by = order_by
-            
+
         self.always_refresh = always_refresh
         self.version_id_col = version_id_col
         self.concrete = concrete
@@ -135,7 +136,7 @@ class Mapper(object):
         self._clause_adapter = None
         self._requires_row_aliasing = False
         self.__inherits_equated_pairs = None
-        
+
         if not issubclass(class_, object):
             raise sa_exc.ArgumentError("Class '%s' is not a new-style class" % class_.__name__)
 
@@ -220,7 +221,7 @@ class Mapper(object):
 
     def has_property(self, key):
         return key in self.__props
-        
+
     def get_property(self, key, resolve_synonyms=False, raiseerr=True):
         """return a MapperProperty associated with the given key."""
 
@@ -347,12 +348,12 @@ class Mapper(object):
         global _new_mappers
         if self.compiled and not _new_mappers:
             return self
-            
+
         _COMPILE_MUTEX.acquire()
         global _already_compiling
         if _already_compiling:
-            # re-entrance to compile() occurs rarely, when a class-mapped construct is 
-            # used within a ForeignKey, something that is possible 
+            # re-entrance to compile() occurs rarely, when a class-mapped construct is
+            # used within a ForeignKey, something that is possible
             # when using the declarative layer
             self.__initialize_properties()
             return
@@ -367,7 +368,7 @@ class Mapper(object):
             for mapper in list(_mapper_registry):
                 if not mapper.compiled:
                     mapper.__initialize_properties()
-                    
+
             _new_mappers = False
             return self
         finally:
@@ -468,7 +469,7 @@ class Mapper(object):
 
             if self.order_by is False and not self.concrete and self.inherits.order_by is not False:
                 self.order_by = self.inherits.order_by
-                
+
             self.polymorphic_map = self.inherits.polymorphic_map
             self.batch = self.inherits.batch
             self.inherits._inheriting_mappers.add(self)
@@ -496,7 +497,7 @@ class Mapper(object):
                     raise sa_exc.ArgumentError("Mapper '%s' specifies a polymorphic_identity of '%s', but no mapper in it's hierarchy specifies the 'polymorphic_on' column argument" % (str(self), self.polymorphic_identity))
                 self.polymorphic_map[self.polymorphic_identity] = self
             self._identity_class = self.class_
-            
+
         if self.mapped_table is None:
             raise sa_exc.ArgumentError("Mapper '%s' does not have a mapped_table specified.  (Are you using the return value of table.create()?  It no longer has a return value.)" % str(self))
 
@@ -571,9 +572,9 @@ class Mapper(object):
         """Create a map of all *equivalent* columns, based on
         the determination of column pairs that are equated to
         one another based on inherit condition.  This is designed
-        to work with the queries that util.polymorphic_union 
+        to work with the queries that util.polymorphic_union
         comes up with, which often don't include the columns from
-        the base table directly (including the subclass table columns 
+        the base table directly (including the subclass table columns
         only).
 
         The resulting structure is a dictionary of columns mapped
@@ -638,15 +639,15 @@ class Mapper(object):
 
     def _should_exclude(self, name, local):
         """determine whether a particular property should be implicitly present on the class.
-        
-        This occurs when properties are propagated from an inherited class, or are 
+
+        This occurs when properties are propagated from an inherited class, or are
         applied from the columns present in the mapped table.
-        
+
         """
-        
+
         def is_userland_descriptor(obj):
             return not isinstance(obj, attributes.InstrumentedAttribute) and hasattr(obj, '__get__')
-            
+
         # check for descriptors, either local or from
         # an inherited class
         if local:
@@ -667,9 +668,9 @@ class Mapper(object):
             name in self.exclude_properties):
             self.__log("excluding property %s" % (name))
             return True
-            
+
         return False
-        
+
     def __compile_properties(self):
 
         # object attribute names mapped to MapperProperty objects
@@ -699,7 +700,7 @@ class Mapper(object):
 
             if self._should_exclude(column.key, local=self.local_table.c.contains_column(column)):
                 continue
-                
+
             column_key = (self.column_prefix or '') + column.key
 
             # adjust the "key" used for this column to that
@@ -707,7 +708,7 @@ class Mapper(object):
             for mapper in self.iterate_to_root():
                 if column in mapper._columntoproperty:
                     column_key = mapper._columntoproperty[column].key
-            
+
             self._compile_property(column_key, column, init=False, setparent=True)
 
         # do a special check for the "discriminiator" column, as it may only be present
@@ -762,13 +763,13 @@ class Mapper(object):
             # columns (included in zblog tests)
             if col is None:
                 col = prop.columns[0]
-                
+
                 # column is coming in after _readonly_props was initialized; check
                 # for 'readonly'
                 if hasattr(self, '_readonly_props') and \
                     (not hasattr(col, 'table') or col.table not in self._cols_by_table):
                         self._readonly_props.add(prop)
-                    
+
             else:
                 # if column is coming in after _cols_by_table was initialized, ensure the col is in the
                 # right set
@@ -792,14 +793,14 @@ class Mapper(object):
 
         self.__props[key] = prop
         prop.key = key
-        
+
         if setparent:
             prop.set_parent(self)
 
             if not self.non_primary:
                 self.class_manager.install_descriptor(
                     key, Mapper._CompileOnAttr(self.class_, key))
-                    
+
         if init:
             prop.init(key, self)
 
@@ -864,10 +865,16 @@ class Mapper(object):
         event_registry = manager.events
         event_registry.add_listener('on_init', _event_on_init)
         event_registry.add_listener('on_init_failure', _event_on_init_failure)
-        if 'on_reconstitute' in self.extension.methods:
-            def reconstitute(instance):
-                self.extension.on_reconstitute(self, instance)
-            event_registry.add_listener('on_load', reconstitute)
+        for key, method in util.iterate_attributes(self.class_):
+            if (isinstance(method, types.FunctionType) and
+                hasattr(method, '__sa_reconstructor__')):
+                event_registry.add_listener('on_load', method)
+                break
+
+        if 'reconstruct_instance' in self.extension.methods:
+            def reconstruct(instance):
+                self.extension.reconstruct_instance(self, instance)
+            event_registry.add_listener('on_load', reconstruct)
 
         manager.info[_INSTRUMENTOR] = self
 
@@ -1219,15 +1226,15 @@ class Mapper(object):
 
                     # testlib.pragma exempt:__hash__
                     inserted_objects.add((state, connection))
-        
+
         if not postupdate:
             for state, mapper, connection, has_identity in tups:
-                
+
                 # expire readonly attributes
                 readonly = state.unmodified.intersection(
                     p.key for p in mapper._readonly_props
                 )
-                
+
                 if readonly:
                     _expire_state(state, readonly)
 
@@ -1238,7 +1245,7 @@ class Mapper(object):
                     uowtransaction.session.query(self)._get(
                         state.key, refresh_state=state,
                         only_load_props=state.unloaded)
-                
+
                 # call after_XXX extensions
                 if not has_identity:
                     if 'after_insert' in mapper.extension.methods:
@@ -1253,10 +1260,10 @@ class Mapper(object):
     def __postfetch(self, uowtransaction, connection, table, state, resultproxy, params, value_params):
         """For a given Table that has just been inserted/updated,
         mark as 'expired' those attributes which correspond to columns
-        that are marked as 'postfetch', and populate attributes which 
+        that are marked as 'postfetch', and populate attributes which
         correspond to columns marked as 'prefetch' or were otherwise generated
         within _save_obj().
-        
+
         """
         postfetch_cols = resultproxy.postfetch_cols()
         generated_cols = list(resultproxy.prefetch_cols())
@@ -1274,7 +1281,7 @@ class Mapper(object):
                 self._set_state_attr_by_column(state, c, params[c.key])
 
         deferred_props = [prop.key for prop in [self._columntoproperty[c] for c in postfetch_cols]]
-        
+
         if deferred_props:
             _expire_state(state, deferred_props)
 
@@ -1462,7 +1469,7 @@ class Mapper(object):
                     identitykey = self._identity_key_from_state(refresh_state)
             else:
                 identitykey = identity_key(row)
-            
+
             if identitykey in session_identity_map:
                 instance = session_identity_map[identitykey]
                 state = attributes.instance_state(instance)
@@ -1538,7 +1545,7 @@ class Mapper(object):
                 # populate attributes on non-loading instances which have been expired
                 # TODO: apply eager loads to un-lazy loaded collections ?
                 if state in context.partials or state.unloaded:
-                        
+
                     if state in context.partials:
                         isnew = False
                         attrs = context.partials[state]
@@ -1588,7 +1595,7 @@ class Mapper(object):
 
         class ColumnsNotAvailable(Exception):
             pass
-            
+
         def visit_binary(binary):
             leftcol = binary.left
             rightcol = binary.right
@@ -1617,13 +1624,33 @@ class Mapper(object):
                     allconds.append(visitors.cloned_traverse(mapper.inherit_condition, {}, {'binary':visit_binary}))
         except ColumnsNotAvailable:
             return None
-            
+
         cond = sql.and_(*allconds)
         return sql.select(tables, cond, use_labels=True)
 
 Mapper.logger = log.class_logger(Mapper)
 
 
+def reconstructor(fn):
+    """Decorate a method as the 'reconstructor' hook.
+
+    Designates a method as the "reconstructor", an ``__init__``-like
+    method that will be called by the ORM after the instance has been
+    loaded from the database or otherwise reconstituted.
+
+    The reconstructor will be invoked with no arguments.  Scalar
+    (non-collection) database-mapped attributes of the instance will
+    be available for use within the function.  Eagerly-loaded
+    collections are generally not yet available and will usually only
+    contain the first element.  ORM state changes made to objects at
+    this stage will not be recorded for the next flush() operation, so
+    the activity within a reconstructor should be conservative.
+
+    """
+    fn.__sa_reconstructor__ = True
+    return fn
+
+
 def _event_on_init(state, instance, args, kwargs):
     """Trigger mapper compilation and run init_instance hooks."""
 
@@ -1654,7 +1681,7 @@ def _load_scalar_attributes(state, attribute_names):
         raise sa_exc.UnboundExecutionError("Instance %s is not bound to a Session; attribute refresh operation cannot proceed" % (state_str(state)))
 
     has_key = _state_has_identity(state)
-    
+
     result = False
     if mapper.inherits and not mapper.concrete:
         statement = mapper._optimized_get_statement(state, attribute_names)
index 5f224955e7041554d86c6fd8a953158354834a24..2f4d9ab5e3c93f55b141d2ef33a333bc6871fa8d 100644 (file)
@@ -301,34 +301,6 @@ class UserDefinedExtensionTest(_base.ORMTest):
         self.assertRaises((AttributeError, KeyError),
                           attributes.instance_state, None)
 
-class ReconstituteTest(testing.TestBase):
-    def test_on_reconstitute(self):
-        recon = []
-        class MyClass(object):
-            @attributes.on_reconstitute
-            def recon(self):
-                recon.append('go')
-        
-        attributes.register_class(MyClass)
-        m = attributes.manager_of_class(MyClass).new_instance()
-        s = attributes.instance_state(m)
-        s._run_on_load(m)
-        assert recon == ['go']
-
-    def test_inheritance(self):
-        recon = []
-        class MyBaseClass(object):
-            @attributes.on_reconstitute
-            def recon(self):
-                recon.append('go')
-        
-        class MySubClass(MyBaseClass):
-            pass
-        attributes.register_class(MySubClass)
-        m = attributes.manager_of_class(MySubClass).new_instance()
-        s = attributes.instance_state(m)
-        s._run_on_load(m)
-        assert recon == ['go']
 
 if __name__ == '__main__':
     testing.main()
index 02db1c8d146a48b9063a2261cf79bcc4bfa81c29..73afd3b9987f731246882f3c1002efedf2d844a8 100644 (file)
@@ -3,7 +3,7 @@
 import testenv; testenv.configure_for_tests()
 from testlib import sa, testing
 from testlib.sa import MetaData, Table, Column, Integer, String, ForeignKey
-from testlib.sa.orm import mapper, relation, backref, create_session, class_mapper
+from testlib.sa.orm import mapper, relation, backref, create_session, class_mapper, reconstructor
 from testlib.sa.orm import defer, deferred, synonym, attributes
 from testlib.testing import eq_
 import pickleable
@@ -765,6 +765,75 @@ class MapperTest(_fixtures.FixtureTest):
             eq_(User.uc_name['key'], 'value')
             sess.rollback()
 
+    @testing.resolve_artifact_names
+    def test_reconstructor(self):
+        recon = []
+
+        class User(object):
+            @reconstructor
+            def reconstruct(self):
+                recon.append('go')
+
+        mapper(User, users)
+
+        User()
+        eq_(recon, [])
+        create_session().query(User).first()
+        eq_(recon, ['go'])
+
+    @testing.resolve_artifact_names
+    def test_reconstructor_inheritance(self):
+        recon = []
+        class A(object):
+            @reconstructor
+            def reconstruct(self):
+                recon.append('A')
+
+        class B(A):
+            @reconstructor
+            def reconstruct(self):
+                recon.append('B')
+
+        class C(A):
+            @reconstructor
+            def reconstruct(self):
+                recon.append('C')
+
+        mapper(A, users, polymorphic_on=users.c.name,
+               polymorphic_identity='jack')
+        mapper(B, inherits=A, polymorphic_identity='ed')
+        mapper(C, inherits=A, polymorphic_identity='chuck')
+
+        A()
+        B()
+        C()
+        eq_(recon, [])
+
+        sess = create_session()
+        sess.query(A).first()
+        sess.query(B).first()
+        sess.query(C).first()
+        eq_(recon, ['A', 'B', 'C'])
+
+    @testing.resolve_artifact_names
+    def test_unmapped_reconstructor_inheritance(self):
+        recon = []
+        class Base(object):
+            @reconstructor
+            def reconstruct(self):
+                recon.append('go')
+
+        class User(Base):
+            pass
+
+        mapper(User, users)
+
+        User()
+        eq_(recon, [])
+
+        create_session().query(User).first()
+        eq_(recon, ['go'])
+
 class OptionsTest(_fixtures.FixtureTest):
 
     @testing.fails_on('maxdb')
@@ -1696,8 +1765,8 @@ class MapperExtensionTest(_fixtures.FixtureTest):
                 methods.append('create_instance')
                 return sa.orm.EXT_CONTINUE
 
-            def on_reconstitute(self, mapper, instance):
-                methods.append('on_reconstitute')
+            def reconstruct_instance(self, mapper, instance):
+                methods.append('reconstruct_instance')
                 return sa.orm.EXT_CONTINUE
 
             def append_result(self, mapper, selectcontext, row, instance, result, **flags):
@@ -1755,8 +1824,8 @@ class MapperExtensionTest(_fixtures.FixtureTest):
             ['instrument_class', 'init_instance', 'before_insert',
              'after_insert', 'translate_row', 'populate_instance',
              'append_result', 'translate_row', 'create_instance',
-             'populate_instance', 'on_reconstitute', 'append_result', 'before_update',
-             'after_update', 'before_delete', 'after_delete'])
+             'populate_instance', 'reconstruct_instance', 'append_result',
+             'before_update', 'after_update', 'before_delete', 'after_delete'])
 
     @testing.resolve_artifact_names
     def test_inheritance(self):
@@ -1783,8 +1852,9 @@ class MapperExtensionTest(_fixtures.FixtureTest):
             ['instrument_class', 'instrument_class', 'init_instance',
              'before_insert', 'after_insert', 'translate_row',
              'populate_instance', 'append_result', 'translate_row',
-             'create_instance', 'populate_instance', 'on_reconstitute', 'append_result',
-             'before_update', 'after_update', 'before_delete', 'after_delete'])
+             'create_instance', 'populate_instance', 'reconstruct_instance',
+             'append_result', 'before_update', 'after_update', 'before_delete',
+             'after_delete'])
 
     @testing.resolve_artifact_names
     def test_after_with_no_changes(self):
@@ -1840,8 +1910,9 @@ class MapperExtensionTest(_fixtures.FixtureTest):
             ['instrument_class', 'instrument_class', 'init_instance',
              'before_insert', 'after_insert', 'translate_row',
              'populate_instance', 'append_result', 'translate_row',
-             'create_instance', 'populate_instance', 'on_reconstitute', 'append_result',
-             'before_update', 'after_update', 'before_delete', 'after_delete'])
+             'create_instance', 'populate_instance', 'reconstruct_instance',
+             'append_result', 'before_update', 'after_update', 'before_delete',
+             'after_delete'])
 
 
 class RequirementsTest(_base.MappedTest):