From: Jason Kirtland Date: Fri, 15 Aug 2008 22:03:42 +0000 (+0000) Subject: - Renamed on_reconstitute to @reconstructor and reconstruct_instance X-Git-Tag: rel_0_5rc1~65 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2d4908e88cce5b9872f20cac66c66efd3a0c98fd;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Renamed on_reconstitute to @reconstructor and reconstruct_instance - Moved @reconstructor hooking to mapper - Expanded reconstructor tests, docs --- diff --git a/CHANGES b/CHANGES index 029728ca56..3bfbfe88e3 100644 --- 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 diff --git a/doc/build/content/mappers.txt b/doc/build/content/mappers.txt index f0821e6e24..11c0382523 100644 --- a/doc/build/content/mappers.txt +++ b/doc/build/content/mappers.txt @@ -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} diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 425a41b371..e405d76a27 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -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', diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 40878be764..9ebe9f79f2 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -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.""" diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 283bc10a5b..0b60483a33 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -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) diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 52acdcb339..ae356126e0 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -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) diff --git a/test/orm/extendedattr.py b/test/orm/extendedattr.py index 5f224955e7..2f4d9ab5e3 100644 --- a/test/orm/extendedattr.py +++ b/test/orm/extendedattr.py @@ -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() diff --git a/test/orm/mapper.py b/test/orm/mapper.py index 02db1c8d14..73afd3b998 100644 --- a/test/orm/mapper.py +++ b/test/orm/mapper.py @@ -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):