summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorJason Kirtland <jek@discorporate.us>2008-08-15 22:03:42 +0000
committerJason Kirtland <jek@discorporate.us>2008-08-15 22:03:42 +0000
commit2d4908e88cce5b9872f20cac66c66efd3a0c98fd (patch)
treea37ceacc17112885559317c042982a9858047077 /lib
parent47f1d414732e1e10b86d9e7bc29946d37446f80e (diff)
downloadsqlalchemy-2d4908e88cce5b9872f20cac66c66efd3a0c98fd.tar.gz
- Renamed on_reconstitute to @reconstructor and reconstruct_instance
- Moved @reconstructor hooking to mapper - Expanded reconstructor tests, docs
Diffstat (limited to 'lib')
-rw-r--r--lib/sqlalchemy/orm/__init__.py2
-rw-r--r--lib/sqlalchemy/orm/attributes.py28
-rw-r--r--lib/sqlalchemy/orm/interfaces.py114
-rw-r--r--lib/sqlalchemy/orm/mapper.py111
4 files changed, 131 insertions, 124 deletions
diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py
index 425a41b37..e405d76a2 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 40878be76..9ebe9f79f 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 283bc10a5..0b60483a3 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 52acdcb33..ae356126e 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)