[Sqlalchemy-commits] [3968] sqlalchemy/trunk: - reworked all lazy/deferred/ expired callables to be
Brought to you by:
zzzeek
From: <co...@sq...> - 2007-12-21 06:57:33
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head><meta http-equiv="content-type" content="text/html; charset=utf-8" /><style type="text/css"><!-- #msg dl { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; } #msg dt { float: left; width: 6em; font-weight: bold; } #msg dt:after { content:':';} #msg dl, #msg dt, #msg ul, #msg li, #header, #footer { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; } #msg dl a { font-weight: bold} #msg dl a:link { color:#fc3; } #msg dl a:active { color:#ff0; } #msg dl a:visited { color:#cc6; } h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; } #msg pre { overflow: auto; background: #ffc; border: 1px #fc0 solid; padding: 6px; } #msg ul, pre { overflow: auto; } #header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; } #patch { width: 100%; } #patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;} #patch .propset h4, #patch .binary h4 {margin:0;} #patch pre {padding:0;line-height:1.2em;margin:0;} #patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;} #patch .propset .diff, #patch .binary .diff {padding:10px 0;} #patch span {display:block;padding:0 10px;} #patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;} #patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;} #patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;} #patch .lines, .info {color:#888;background:#fff;} --></style> <title>[3968] sqlalchemy/trunk: - reworked all lazy/deferred/ expired callables to be </title> </head> <body> <div id="msg"> <dl> <dt>Revision</dt> <dd>3968</dd> <dt>Author</dt> <dd>zzzeek</dd> <dt>Date</dt> <dd>2007-12-21 01:57:20 -0500 (Fri, 21 Dec 2007)</dd> </dl> <h3>Log Message</h3> <pre>- reworked all lazy/deferred/expired callables to be serializable class instances, added pickling tests - cleaned up "deferred" polymorphic system so that the mapper handles it entirely - columns which are missing from a Query's select statement now get automatically deferred during load.</pre> <h3>Modified Paths</h3> <ul> <li><a href="#sqlalchemytrunkCHANGES">sqlalchemy/trunk/CHANGES</a></li> <li><a href="#sqlalchemytrunklibsqlalchemyormattributespy">sqlalchemy/trunk/lib/sqlalchemy/orm/attributes.py</a></li> <li><a href="#sqlalchemytrunklibsqlalchemyorminterfacespy">sqlalchemy/trunk/lib/sqlalchemy/orm/interfaces.py</a></li> <li><a href="#sqlalchemytrunklibsqlalchemyormmapperpy">sqlalchemy/trunk/lib/sqlalchemy/orm/mapper.py</a></li> <li><a href="#sqlalchemytrunklibsqlalchemyormsessionpy">sqlalchemy/trunk/lib/sqlalchemy/orm/session.py</a></li> <li><a href="#sqlalchemytrunklibsqlalchemyormstrategiespy">sqlalchemy/trunk/lib/sqlalchemy/orm/strategies.py</a></li> <li><a href="#sqlalchemytrunklibsqlalchemyormutilpy">sqlalchemy/trunk/lib/sqlalchemy/orm/util.py</a></li> <li><a href="#sqlalchemytrunklibsqlalchemysqlexpressionpy">sqlalchemy/trunk/lib/sqlalchemy/sql/expression.py</a></li> <li><a href="#sqlalchemytrunktestormalltestspy">sqlalchemy/trunk/test/orm/alltests.py</a></li> <li><a href="#sqlalchemytrunktestormattributespy">sqlalchemy/trunk/test/orm/attributes.py</a></li> <li><a href="#sqlalchemytrunktestormexpirepy">sqlalchemy/trunk/test/orm/expire.py</a></li> <li><a href="#sqlalchemytrunktestormmapperpy">sqlalchemy/trunk/test/orm/mapper.py</a></li> </ul> <h3>Added Paths</h3> <ul> <li><a href="#sqlalchemytrunktestormpickledpy">sqlalchemy/trunk/test/orm/pickled.py</a></li> </ul> </div> <div id="patch"> <h3>Diff</h3> <a id="sqlalchemytrunkCHANGES"></a> <div class="modfile"><h4>Modified: sqlalchemy/trunk/CHANGES (3967 => 3968)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/trunk/CHANGES 2007-12-20 02:37:48 UTC (rev 3967) +++ sqlalchemy/trunk/CHANGES 2007-12-21 06:57:20 UTC (rev 3968) </span><span class="lines">@@ -99,7 +99,14 @@ </span><span class="cx"> have each method called only once per operation, use the same </span><span class="cx"> instance of the extension for both mappers. </span><span class="cx"> [ticket:490] </span><ins>+ + - columns which are missing from a Query's select statement + now get automatically deferred during load. </ins><span class="cx"> </span><ins>+ - improved support for pickling of mapped entities. Per-instance + lazy/deferred/expired callables are now serializable so that + they serialize and deserialize with _state. + </ins><span class="cx"> - new synonym() behavior: an attribute will be placed on the mapped </span><span class="cx"> class, if one does not exist already, in all cases. if a property </span><span class="cx"> already exists on the class, the synonym will decorate the property </span></span></pre></div> <a id="sqlalchemytrunklibsqlalchemyormattributespy"></a> <div class="modfile"><h4>Modified: sqlalchemy/trunk/lib/sqlalchemy/orm/attributes.py (3967 => 3968)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/trunk/lib/sqlalchemy/orm/attributes.py 2007-12-20 02:37:48 UTC (rev 3967) +++ sqlalchemy/trunk/lib/sqlalchemy/orm/attributes.py 2007-12-21 06:57:20 UTC (rev 3968) </span><span class="lines">@@ -255,12 +255,13 @@ </span><span class="cx"> class ScalarAttributeImpl(AttributeImpl): </span><span class="cx"> """represents a scalar value-holding InstrumentedAttribute.""" </span><span class="cx"> </span><del>- accepts_global_callable = True </del><ins>+ accepts_scalar_loader = True </ins><span class="cx"> </span><span class="cx"> def delete(self, state): </span><span class="cx"> if self.key not in state.committed_state: </span><span class="cx"> state.committed_state[self.key] = state.dict.get(self.key, NO_VALUE) </span><span class="cx"> </span><ins>+ # TODO: catch key errors, convert to attributeerror? </ins><span class="cx"> del state.dict[self.key] </span><span class="cx"> state.modified=True </span><span class="cx"> </span><span class="lines">@@ -327,7 +328,7 @@ </span><span class="cx"> Adds events to delete/set operations. </span><span class="cx"> """ </span><span class="cx"> </span><del>- accepts_global_callable = False </del><ins>+ accepts_scalar_loader = False </ins><span class="cx"> </span><span class="cx"> def __init__(self, class_, key, callable_, trackparent=False, extension=None, copy_function=None, compare_function=None, **kwargs): </span><span class="cx"> super(ScalarObjectAttributeImpl, self).__init__(class_, key, </span><span class="lines">@@ -338,6 +339,7 @@ </span><span class="cx"> </span><span class="cx"> def delete(self, state): </span><span class="cx"> old = self.get(state) </span><ins>+ # TODO: catch key errors, convert to attributeerror? </ins><span class="cx"> del state.dict[self.key] </span><span class="cx"> self.fire_remove_event(state, old, self) </span><span class="cx"> </span><span class="lines">@@ -404,7 +406,7 @@ </span><span class="cx"> CollectionAdapter, a "view" onto that object that presents consistent </span><span class="cx"> bag semantics to the orm layer independent of the user data implementation. </span><span class="cx"> """ </span><del>- accepts_global_callable = False </del><ins>+ accepts_scalar_loader = False </ins><span class="cx"> </span><span class="cx"> def __init__(self, class_, key, callable_, typecallable=None, trackparent=False, extension=None, copy_function=None, compare_function=None, **kwargs): </span><span class="cx"> super(CollectionAttributeImpl, self).__init__(class_, </span><span class="lines">@@ -479,6 +481,7 @@ </span><span class="cx"> </span><span class="cx"> collection = self.get_collection(state) </span><span class="cx"> collection.clear_with_event() </span><ins>+ # TODO: catch key errors, convert to attributeerror? </ins><span class="cx"> del state.dict[self.key] </span><span class="cx"> </span><span class="cx"> def initialize(self, state): </span><span class="lines">@@ -648,7 +651,7 @@ </span><span class="cx"> self.mappers = {} </span><span class="cx"> self.attrs = {} </span><span class="cx"> self.has_mutable_scalars = False </span><del>- </del><ins>+ </ins><span class="cx"> class InstanceState(object): </span><span class="cx"> """tracks state information at the instance level.""" </span><span class="cx"> </span><span class="lines">@@ -658,7 +661,6 @@ </span><span class="cx"> self.dict = obj.__dict__ </span><span class="cx"> self.committed_state = {} </span><span class="cx"> self.modified = False </span><del>- self.trigger = None </del><span class="cx"> self.callables = {} </span><span class="cx"> self.parents = {} </span><span class="cx"> self.pending = {} </span><span class="lines">@@ -735,7 +737,7 @@ </span><span class="cx"> return None </span><span class="cx"> </span><span class="cx"> def __getstate__(self): </span><del>- return {'committed_state':self.committed_state, 'pending':self.pending, 'parents':self.parents, 'modified':self.modified, 'instance':self.obj()} </del><ins>+ return {'committed_state':self.committed_state, 'pending':self.pending, 'parents':self.parents, 'modified':self.modified, 'instance':self.obj(), 'expired_attributes':getattr(self, 'expired_attributes', None), 'callables':self.callables} </ins><span class="cx"> </span><span class="cx"> def __setstate__(self, state): </span><span class="cx"> self.committed_state = state['committed_state'] </span><span class="lines">@@ -745,43 +747,62 @@ </span><span class="cx"> self.obj = weakref.ref(state['instance']) </span><span class="cx"> self.class_ = self.obj().__class__ </span><span class="cx"> self.dict = self.obj().__dict__ </span><del>- self.callables = {} - self.trigger = None - </del><ins>+ self.callables = state['callables'] + self.runid = None + self.appenders = {} + if state['expired_attributes'] is not None: + self.expire_attributes(state['expired_attributes']) + </ins><span class="cx"> def initialize(self, key): </span><span class="cx"> getattr(self.class_, key).impl.initialize(self) </span><span class="cx"> </span><span class="cx"> def set_callable(self, key, callable_): </span><span class="cx"> self.dict.pop(key, None) </span><span class="cx"> self.callables[key] = callable_ </span><del>- - def __fire_trigger(self): </del><ins>+ + def __call__(self): + """__call__ allows the InstanceState to act as a deferred + callable for loading expired attributes, which is also + serializable. + """ </ins><span class="cx"> instance = self.obj() </span><del>- self.trigger(instance, [k for k in self.expired_attributes if k not in self.dict]) </del><ins>+ self.class_._class_state.deferred_scalar_loader(instance, [k for k in self.expired_attributes if k not in self.committed_state]) </ins><span class="cx"> for k in self.expired_attributes: </span><span class="cx"> self.callables.pop(k, None) </span><span class="cx"> self.expired_attributes.clear() </span><span class="cx"> return ATTR_WAS_SET </span><span class="cx"> </span><ins>+ def unmodified(self): + """a set of keys which have no uncommitted changes""" + + return util.Set([ + attr.impl.key for attr in _managed_attributes(self.class_) if + attr.impl.key not in self.committed_state + and (not hasattr(attr.impl, 'commit_to_state') or not attr.impl.check_mutable_modified(self)) + ]) + unmodified = property(unmodified) + </ins><span class="cx"> def expire_attributes(self, attribute_names): </span><span class="cx"> if not hasattr(self, 'expired_attributes'): </span><span class="cx"> self.expired_attributes = util.Set() </span><ins>+ </ins><span class="cx"> if attribute_names is None: </span><span class="cx"> for attr in _managed_attributes(self.class_): </span><span class="cx"> self.dict.pop(attr.impl.key, None) </span><del>- self.callables[attr.impl.key] = self.__fire_trigger - self.expired_attributes.add(attr.impl.key) </del><ins>+ + if attr.impl.accepts_scalar_loader: + self.callables[attr.impl.key] = self + self.expired_attributes.add(attr.impl.key) + </ins><span class="cx"> self.committed_state = {} </span><span class="cx"> else: </span><span class="cx"> for key in attribute_names: </span><span class="cx"> self.dict.pop(key, None) </span><span class="cx"> self.committed_state.pop(key, None) </span><span class="cx"> </span><del>- if not getattr(self.class_, key).impl.accepts_global_callable: - continue - - self.callables[key] = self.__fire_trigger - self.expired_attributes.add(key) </del><ins>+ if getattr(self.class_, key).impl.accepts_scalar_loader: + self.callables[key] = self + self.expired_attributes.add(key) </ins><span class="cx"> </span><span class="cx"> def reset(self, key): </span><span class="cx"> """remove the given attribute and any callables associated with it.""" </span><span class="lines">@@ -1081,7 +1102,7 @@ </span><span class="cx"> if not '_class_state' in class_.__dict__: </span><span class="cx"> class_._class_state = ClassState() </span><span class="cx"> </span><del>-def register_class(class_, extra_init=None, on_exception=None): </del><ins>+def register_class(class_, extra_init=None, on_exception=None, deferred_scalar_loader=None): </ins><span class="cx"> # do a sweep first, this also helps some attribute extensions </span><span class="cx"> # (like associationproxy) become aware of themselves at the </span><span class="cx"> # class level </span><span class="lines">@@ -1089,6 +1110,7 @@ </span><span class="cx"> getattr(class_, key, None) </span><span class="cx"> </span><span class="cx"> _init_class_state(class_) </span><ins>+ class_._class_state.deferred_scalar_loader=deferred_scalar_loader </ins><span class="cx"> </span><span class="cx"> oldinit = None </span><span class="cx"> doinit = False </span></span></pre></div> <a id="sqlalchemytrunklibsqlalchemyorminterfacespy"></a> <div class="modfile"><h4>Modified: sqlalchemy/trunk/lib/sqlalchemy/orm/interfaces.py (3967 => 3968)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/trunk/lib/sqlalchemy/orm/interfaces.py 2007-12-20 02:37:48 UTC (rev 3967) +++ sqlalchemy/trunk/lib/sqlalchemy/orm/interfaces.py 2007-12-21 06:57:20 UTC (rev 3968) </span><span class="lines">@@ -15,6 +15,7 @@ </span><span class="cx"> """ </span><span class="cx"> from sqlalchemy import util, logging, exceptions </span><span class="cx"> from sqlalchemy.sql import expression </span><ins>+from itertools import chain </ins><span class="cx"> class_mapper = None </span><span class="cx"> </span><span class="cx"> __all__ = ['EXT_CONTINUE', 'EXT_STOP', 'EXT_PASS', 'MapperExtension', </span><span class="lines">@@ -505,8 +506,28 @@ </span><span class="cx"> return prev + (mapper.base_mapper, key) </span><span class="cx"> else: </span><span class="cx"> return (mapper.base_mapper, key) </span><del>- </del><span class="cx"> </span><ins>+def serialize_path(path): + if path is None: + return None + + return [ + (mapper.class_, mapper.entity_name, key) + for mapper, key in [(path[i], path[i+1]) for i in range(0, len(path)-1, 2)] + ] + +def deserialize_path(path): + if path is None: + return None + + global class_mapper + if class_mapper is None: + from sqlalchemy.orm import class_mapper + + return tuple( + chain(*[(class_mapper(cls, entity), key) for cls, entity, key in path]) + ) + </ins><span class="cx"> class MapperOption(object): </span><span class="cx"> """Describe a modification to a Query.""" </span><span class="cx"> </span></span></pre></div> <a id="sqlalchemytrunklibsqlalchemyormmapperpy"></a> <div class="modfile"><h4>Modified: sqlalchemy/trunk/lib/sqlalchemy/orm/mapper.py (3967 => 3968)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/trunk/lib/sqlalchemy/orm/mapper.py 2007-12-20 02:37:48 UTC (rev 3967) +++ sqlalchemy/trunk/lib/sqlalchemy/orm/mapper.py 2007-12-21 06:57:20 UTC (rev 3968) </span><span class="lines">@@ -758,7 +758,7 @@ </span><span class="cx"> def on_exception(class_, oldinit, instance, args, kwargs): </span><span class="cx"> util.warn_exception(self.extension.init_failed, self, class_, oldinit, instance, args, kwargs) </span><span class="cx"> </span><del>- attributes.register_class(self.class_, extra_init=extra_init, on_exception=on_exception) </del><ins>+ attributes.register_class(self.class_, extra_init=extra_init, on_exception=on_exception, deferred_scalar_loader=_load_scalar_attributes) </ins><span class="cx"> </span><span class="cx"> self._class_state = self.class_._class_state </span><span class="cx"> _mapper_registry[self] = True </span><span class="lines">@@ -1358,43 +1358,23 @@ </span><span class="cx"> instance._sa_session_id = context.session.hash_key </span><span class="cx"> session_identity_map[identitykey] = instance </span><span class="cx"> </span><del>- if currentload or context.populate_existing or self.always_refresh or state.trigger: </del><ins>+ if currentload or context.populate_existing or self.always_refresh: </ins><span class="cx"> if isnew: </span><span class="cx"> state.runid = context.runid </span><del>- state.trigger = None </del><span class="cx"> context.progress.add(state) </span><del>- </del><ins>+ </ins><span class="cx"> if 'populate_instance' not in extension.methods or extension.populate_instance(self, context, row, instance, only_load_props=only_load_props, instancekey=identitykey, isnew=isnew) is EXT_CONTINUE: </span><span class="cx"> self.populate_instance(context, instance, row, only_load_props=only_load_props, instancekey=identitykey, isnew=isnew) </span><del>- </del><ins>+ + elif getattr(state, 'expired_attributes', None): + if 'populate_instance' not in extension.methods or extension.populate_instance(self, context, row, instance, only_load_props=state.expired_attributes, instancekey=identitykey, isnew=isnew) is EXT_CONTINUE: + self.populate_instance(context, instance, row, only_load_props=state.expired_attributes, instancekey=identitykey, isnew=isnew) + </ins><span class="cx"> if result is not None and ('append_result' not in extension.methods or extension.append_result(self, context, row, instance, result, instancekey=identitykey, isnew=isnew) is EXT_CONTINUE): </span><span class="cx"> result.append(instance) </span><span class="cx"> </span><span class="cx"> return instance </span><del>- - def _deferred_inheritance_condition(self, base_mapper, needs_tables): - def visit_binary(binary): - leftcol = binary.left - rightcol = binary.right - if leftcol is None or rightcol is None: - return - if leftcol.table not in needs_tables: - binary.left = sql.bindparam(None, None, type_=binary.right.type) - param_names.append((leftcol, binary.left)) - elif rightcol not in needs_tables: - binary.right = sql.bindparam(None, None, type_=binary.right.type) - param_names.append((rightcol, binary.right)) </del><span class="cx"> </span><del>- allconds = [] - param_names = [] - - for mapper in self.iterate_to_root(): - if mapper is base_mapper: - break - allconds.append(visitors.traverse(mapper.inherit_condition, clone=True, visit_binary=visit_binary)) - - return sql.and_(*allconds), param_names - </del><span class="cx"> def translate_row(self, tomapper, row): </span><span class="cx"> """Translate the column keys of a row into a new or proxied </span><span class="cx"> row that can be understood by another mapper. </span><span class="lines">@@ -1451,7 +1431,10 @@ </span><span class="cx"> populators = new_populators </span><span class="cx"> else: </span><span class="cx"> populators = existing_populators </span><del>- </del><ins>+ + if only_load_props: + populators = [p for p in populators if p[0] in only_load_props] + </ins><span class="cx"> for (key, populator) in populators: </span><span class="cx"> selectcontext.exec_with_path(self, key, populator, instance, row, ispostselect=ispostselect, isnew=isnew, **flags) </span><span class="cx"> </span><span class="lines">@@ -1464,26 +1447,75 @@ </span><span class="cx"> p(state.obj()) </span><span class="cx"> </span><span class="cx"> def _get_poly_select_loader(self, selectcontext, row): </span><del>- # 'select' or 'union'+col not present </del><ins>+ """set up attribute loaders for 'select' and 'deferred' polymorphic loading. + + this loading uses a second SELECT statement to load additional tables, + either immediately after loading the main table or via a deferred attribute trigger. + """ + </ins><span class="cx"> (hosted_mapper, needs_tables) = selectcontext.attributes.get(('polymorphic_fetch', self), (None, None)) </span><del>- if hosted_mapper is None or not needs_tables or hosted_mapper.polymorphic_fetch == 'deferred': </del><ins>+ + if hosted_mapper is None or not needs_tables: </ins><span class="cx"> return </span><span class="cx"> </span><span class="cx"> cond, param_names = self._deferred_inheritance_condition(hosted_mapper, needs_tables) </span><span class="cx"> statement = sql.select(needs_tables, cond, use_labels=True) </span><del>- def post_execute(instance, **flags): - if self.__should_log_debug: - self.__log_debug("Post query loading instance " + instance_str(instance)) </del><ins>+ + if hosted_mapper.polymorphic_fetch == 'select': + def post_execute(instance, **flags): + if self.__should_log_debug: + self.__log_debug("Post query loading instance " + instance_str(instance)) </ins><span class="cx"> </span><del>- identitykey = self.identity_key_from_instance(instance) </del><ins>+ identitykey = self.identity_key_from_instance(instance) </ins><span class="cx"> </span><del>- params = {} - for c, bind in param_names: - params[bind] = self._get_attr_by_column(instance, c) - row = selectcontext.session.connection(self).execute(statement, params).fetchone() - self.populate_instance(selectcontext, instance, row, isnew=False, instancekey=identitykey, ispostselect=True) </del><ins>+ params = {} + for c, bind in param_names: + params[bind] = self._get_attr_by_column(instance, c) + row = selectcontext.session.connection(self).execute(statement, params).fetchone() + self.populate_instance(selectcontext, instance, row, isnew=False, instancekey=identitykey, ispostselect=True) + return post_execute + elif hosted_mapper.polymorphic_fetch == 'deferred': + from sqlalchemy.orm.strategies import DeferredColumnLoader + + def post_execute(instance, **flags): + def create_statement(instance): + params = {} + for (c, bind) in param_names: + # use the "committed" (database) version to get query column values + params[bind] = self._get_committed_attr_by_column(instance, c) + return (statement, params) + + props = [prop for prop in [self._get_col_to_prop(col) for col in statement.inner_columns] if prop.key not in instance.__dict__] + keys = [p.key for p in props] + for prop in props: + strategy = prop._get_strategy(DeferredColumnLoader) + instance._state.set_callable(prop.key, strategy.setup_loader(instance, props=keys, create_statement=create_statement)) + return post_execute + else: + return None </ins><span class="cx"> </span><del>- return post_execute </del><ins>+ def _deferred_inheritance_condition(self, base_mapper, needs_tables): + def visit_binary(binary): + leftcol = binary.left + rightcol = binary.right + if leftcol is None or rightcol is None: + return + if leftcol.table not in needs_tables: + binary.left = sql.bindparam(None, None, type_=binary.right.type) + param_names.append((leftcol, binary.left)) + elif rightcol not in needs_tables: + binary.right = sql.bindparam(None, None, type_=binary.right.type) + param_names.append((rightcol, binary.right)) + + allconds = [] + param_names = [] + + for mapper in self.iterate_to_root(): + if mapper is base_mapper: + break + allconds.append(visitors.traverse(mapper.inherit_condition, clone=True, visit_binary=visit_binary)) + + return sql.and_(*allconds), param_names </ins><span class="cx"> </span><span class="cx"> Mapper.logger = logging.class_logger(Mapper) </span><span class="cx"> </span><span class="lines">@@ -1501,6 +1533,16 @@ </span><span class="cx"> </span><span class="cx"> return hasattr(object, '_entity_name') </span><span class="cx"> </span><ins>+object_session = None + +def _load_scalar_attributes(instance, attribute_names): + global object_session + if not object_session: + from sqlalchemy.orm.session import object_session + + if object_session(instance).query(object_mapper(instance))._get(instance._instance_key, refresh_instance=instance._state, only_load_props=attribute_names) is None: + raise exceptions.InvalidRequestError("Could not refresh instance '%s'" % instance_str(instance)) + </ins><span class="cx"> def _state_mapper(state, entity_name=None): </span><span class="cx"> return state.class_._class_state.mappers[state.dict.get('_entity_name', entity_name)] </span><span class="cx"> </span></span></pre></div> <a id="sqlalchemytrunklibsqlalchemyormsessionpy"></a> <div class="modfile"><h4>Modified: sqlalchemy/trunk/lib/sqlalchemy/orm/session.py (3967 => 3968)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/trunk/lib/sqlalchemy/orm/session.py 2007-12-20 02:37:48 UTC (rev 3967) +++ sqlalchemy/trunk/lib/sqlalchemy/orm/session.py 2007-12-21 06:57:20 UTC (rev 3968) </span><span class="lines">@@ -1113,7 +1113,7 @@ </span><span class="cx"> </span><span class="cx"> return util.IdentitySet(self.uow.new.values()) </span><span class="cx"> new = property(new) </span><del>- </del><ins>+ </ins><span class="cx"> def _expire_state(state, attribute_names): </span><span class="cx"> """Standalone expire instance function. </span><span class="cx"> </span><span class="lines">@@ -1124,12 +1124,6 @@ </span><span class="cx"> If the list is None or blank, the entire instance is expired. </span><span class="cx"> """ </span><span class="cx"> </span><del>- if state.trigger is None: - def load_attributes(instance, attribute_names): - if object_session(instance).query(instance.__class__)._get(instance._instance_key, refresh_instance=instance._state, only_load_props=attribute_names) is None: - raise exceptions.InvalidRequestError("Could not refresh instance '%s'" % mapperutil.instance_str(instance)) - state.trigger = load_attributes - </del><span class="cx"> state.expire_attributes(attribute_names) </span><span class="cx"> </span><span class="cx"> register_attribute = unitofwork.register_attribute </span></span></pre></div> <a id="sqlalchemytrunklibsqlalchemyormstrategiespy"></a> <div class="modfile"><h4>Modified: sqlalchemy/trunk/lib/sqlalchemy/orm/strategies.py (3967 => 3968)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/trunk/lib/sqlalchemy/orm/strategies.py 2007-12-20 02:37:48 UTC (rev 3967) +++ sqlalchemy/trunk/lib/sqlalchemy/orm/strategies.py 2007-12-21 06:57:20 UTC (rev 3968) </span><span class="lines">@@ -10,7 +10,7 @@ </span><span class="cx"> from sqlalchemy.sql import util as sql_util </span><span class="cx"> from sqlalchemy.sql import visitors, expression, operators </span><span class="cx"> from sqlalchemy.orm import mapper, attributes </span><del>-from sqlalchemy.orm.interfaces import LoaderStrategy, StrategizedOption, MapperOption, PropertyOption </del><ins>+from sqlalchemy.orm.interfaces import LoaderStrategy, StrategizedOption, MapperOption, PropertyOption, serialize_path, deserialize_path </ins><span class="cx"> from sqlalchemy.orm import session as sessionlib </span><span class="cx"> from sqlalchemy.orm import util as mapperutil </span><span class="cx"> </span><span class="lines">@@ -80,54 +80,14 @@ </span><span class="cx"> if self._should_log_debug: </span><span class="cx"> self.logger.debug("Returning active column fetcher for %s %s" % (mapper, self.key)) </span><span class="cx"> return (new_execute, None, None) </span><del>- - # our mapped column is not present in the row. check if we need to initialize a polymorphic - # row fetcher used by inheritance. - (hosted_mapper, needs_tables) = selectcontext.attributes.get(('polymorphic_fetch', mapper), (None, None)) - - if hosted_mapper is None: - return (None, None, None) - - if hosted_mapper.polymorphic_fetch == 'deferred': - # 'deferred' polymorphic row fetcher, put a callable on the property. - # create a deferred column loader which will query the remaining not-yet-loaded tables in an inheritance load. - # the mapper for the object creates the WHERE criterion using the mapper who originally - # "hosted" the query and the list of tables which are unloaded between the "hosted" mapper - # and this mapper. (i.e. A->B->C, the query used mapper A. therefore will need B's and C's tables - # in the query). - - # deferred loader strategy - strategy = self.parent_property._get_strategy(DeferredColumnLoader) - - # full list of ColumnProperty objects to be loaded in the deferred fetch - props = [p.key for p in mapper.iterate_properties if isinstance(p.strategy, ColumnLoader) and p.columns[0].table in needs_tables] - - # TODO: we are somewhat duplicating efforts from mapper._get_poly_select_loader - # and should look for ways to simplify. - cond, param_names = mapper._deferred_inheritance_condition(hosted_mapper, needs_tables) - statement = sql.select(needs_tables, cond, use_labels=True) - def create_statement(instance): - params = {} - for (c, bind) in param_names: - # use the "committed" (database) version to get query column values - params[bind] = mapper._get_committed_attr_by_column(instance, c) - return (statement, params) - </del><ins>+ else: </ins><span class="cx"> def new_execute(instance, row, isnew, **flags): </span><span class="cx"> if isnew: </span><del>- instance._state.set_callable(self.key, strategy.setup_loader(instance, props=props, create_statement=create_statement)) - </del><ins>+ instance._state.expire_attributes([self.key]) </ins><span class="cx"> if self._should_log_debug: </span><del>- self.logger.debug("Returning deferred column fetcher for %s %s" % (mapper, self.key)) - </del><ins>+ self.logger.debug("Deferring load for %s %s" % (mapper, self.key)) </ins><span class="cx"> return (new_execute, None, None) </span><del>- else: - # immediate polymorphic row fetcher. no processing needed for this row. - if self._should_log_debug: - self.logger.debug("Returning no column fetcher for %s %s" % (mapper, self.key)) - return (None, None, None) </del><span class="cx"> </span><del>- </del><span class="cx"> ColumnLoader.logger = logging.class_logger(ColumnLoader) </span><span class="cx"> </span><span class="cx"> class DeferredColumnLoader(LoaderStrategy): </span><span class="lines">@@ -170,9 +130,10 @@ </span><span class="cx"> self.parent_property._get_strategy(ColumnLoader).setup_query(context, **kwargs) </span><span class="cx"> </span><span class="cx"> def setup_loader(self, instance, props=None, create_statement=None): </span><del>- localparent = mapper.object_mapper(instance, raiseerror=False) - if localparent is None: </del><ins>+ if not mapper.has_mapper(instance): </ins><span class="cx"> return None </span><ins>+ + localparent = mapper.object_mapper(instance) </ins><span class="cx"> </span><span class="cx"> # adjust for the ColumnProperty associated with the instance </span><span class="cx"> # not being our own ColumnProperty. This can occur when entity_name </span><span class="lines">@@ -181,39 +142,64 @@ </span><span class="cx"> prop = localparent.get_property(self.key) </span><span class="cx"> if prop is not self.parent_property: </span><span class="cx"> return prop._get_strategy(DeferredColumnLoader).setup_loader(instance) </span><del>- - def lazyload(): - if not mapper.has_identity(instance): - return None - - if props is not None: - group = props - elif self.group is not None: - group = [p.key for p in localparent.iterate_properties if isinstance(p.strategy, DeferredColumnLoader) and p.group==self.group] - else: - group = [self.parent_property.key] - - # narrow the keys down to just those which aren't present on the instance - group = [k for k in group if k not in instance.__dict__] - - if self._should_log_debug: - self.logger.debug("deferred load %s group %s" % (mapperutil.attribute_str(instance, self.key), group and ','.join(group) or 'None')) </del><span class="cx"> </span><del>- session = sessionlib.object_session(instance) - if session is None: - raise exceptions.InvalidRequestError("Parent instance %s is not bound to a Session; deferred load operation of attribute '%s' cannot proceed" % (instance.__class__, self.key)) - - if create_statement is None: - ident = instance._instance_key[1] - session.query(localparent)._get(None, ident=ident, only_load_props=group, refresh_instance=instance._state) - else: - statement, params = create_statement(instance) - session.query(localparent).from_statement(statement).params(params)._get(None, only_load_props=group, refresh_instance=instance._state) - return attributes.ATTR_WAS_SET - return lazyload </del><ins>+ return LoadDeferredColumns(instance, self.key, props, optimizing_statement=create_statement) </ins><span class="cx"> </span><span class="cx"> DeferredColumnLoader.logger = logging.class_logger(DeferredColumnLoader) </span><span class="cx"> </span><ins>+class LoadDeferredColumns(object): + """callable, serializable loader object used by DeferredColumnLoader""" + + def __init__(self, instance, key, keys, optimizing_statement): + self.instance = instance + self.key = key + self.keys = keys + self.optimizing_statement = optimizing_statement + + def __getstate__(self): + return {'instance':self.instance, 'key':self.key, 'keys':self.keys} + + def __setstate__(self, state): + self.instance = state['instance'] + self.key = state['key'] + self.keys = state['keys'] + self.optimizing_statement = None + + def __call__(self): + if not mapper.has_identity(self.instance): + return None + + localparent = mapper.object_mapper(self.instance, raiseerror=False) + + prop = localparent.get_property(self.key) + strategy = prop._get_strategy(DeferredColumnLoader) + + if self.keys: + toload = self.keys + elif strategy.group: + toload = [p.key for p in localparent.iterate_properties if isinstance(p.strategy, DeferredColumnLoader) and p.group==strategy.group] + else: + toload = [self.key] + + # narrow the keys down to just those which have no history + group = [k for k in toload if k in self.instance._state.unmodified] + + if strategy._should_log_debug: + strategy.logger.debug("deferred load %s group %s" % (mapperutil.attribute_str(self.instance, self.key), group and ','.join(group) or 'None')) + + session = sessionlib.object_session(self.instance) + if session is None: + raise exceptions.InvalidRequestError("Parent instance %s is not bound to a Session; deferred load operation of attribute '%s' cannot proceed" % (self.instance.__class__, self.key)) + + query = session.query(localparent) + if not self.optimizing_statement: + ident = self.instance._instance_key[1] + query._get(None, ident=ident, only_load_props=group, refresh_instance=self.instance._state) + else: + statement, params = self.optimizing_statement(self.instance) + query.from_statement(statement).params(params)._get(None, only_load_props=group, refresh_instance=self.instance._state) + return attributes.ATTR_WAS_SET + </ins><span class="cx"> class DeferredOption(StrategizedOption): </span><span class="cx"> def __init__(self, key, defer=False): </span><span class="cx"> super(DeferredOption, self).__init__(key) </span><span class="lines">@@ -276,7 +262,7 @@ </span><span class="cx"> class LazyLoader(AbstractRelationLoader): </span><span class="cx"> def init(self): </span><span class="cx"> super(LazyLoader, self).init() </span><del>- (self.lazywhere, self.lazybinds, self.lazyreverse) = self._create_lazy_clause(self) </del><ins>+ (self.lazywhere, self.lazybinds, self.equated_columns) = self._create_lazy_clause(self) </ins><span class="cx"> </span><span class="cx"> self.logger.info(str(self.parent_property) + " lazy loading clause " + str(self.lazywhere)) </span><span class="cx"> </span><span class="lines">@@ -293,10 +279,10 @@ </span><span class="cx"> </span><span class="cx"> def lazy_clause(self, instance, reverse_direction=False): </span><span class="cx"> if instance is None: </span><del>- return self.lazy_none_clause(reverse_direction) </del><ins>+ return self._lazy_none_clause(reverse_direction) </ins><span class="cx"> </span><span class="cx"> if not reverse_direction: </span><del>- (criterion, lazybinds, rev) = (self.lazywhere, self.lazybinds, self.lazyreverse) </del><ins>+ (criterion, lazybinds, rev) = (self.lazywhere, self.lazybinds, self.equated_columns) </ins><span class="cx"> else: </span><span class="cx"> (criterion, lazybinds, rev) = LazyLoader._create_lazy_clause(self.parent_property, reverse_direction=reverse_direction) </span><span class="cx"> bind_to_col = dict([(lazybinds[col].key, col) for col in lazybinds]) </span><span class="lines">@@ -308,9 +294,9 @@ </span><span class="cx"> bindparam.value = mapper._get_committed_attr_by_column(instance, bind_to_col[bindparam.key]) </span><span class="cx"> return visitors.traverse(criterion, clone=True, visit_bindparam=visit_bindparam) </span><span class="cx"> </span><del>- def lazy_none_clause(self, reverse_direction=False): </del><ins>+ def _lazy_none_clause(self, reverse_direction=False): </ins><span class="cx"> if not reverse_direction: </span><del>- (criterion, lazybinds, rev) = (self.lazywhere, self.lazybinds, self.lazyreverse) </del><ins>+ (criterion, lazybinds, rev) = (self.lazywhere, self.lazybinds, self.equated_columns) </ins><span class="cx"> else: </span><span class="cx"> (criterion, lazybinds, rev) = LazyLoader._create_lazy_clause(self.parent_property, reverse_direction=reverse_direction) </span><span class="cx"> bind_to_col = dict([(lazybinds[col].key, col) for col in lazybinds]) </span><span class="lines">@@ -331,72 +317,19 @@ </span><span class="cx"> def setup_loader(self, instance, options=None, path=None): </span><span class="cx"> if not mapper.has_mapper(instance): </span><span class="cx"> return None </span><del>- else: - # adjust for the PropertyLoader associated with the instance - # not being our own PropertyLoader. This can occur when entity_name - # mappers are used to map different versions of the same PropertyLoader - # to the class. - prop = mapper.object_mapper(instance).get_property(self.key) - if prop is not self.parent_property: - return prop._get_strategy(LazyLoader).setup_loader(instance) </del><span class="cx"> </span><del>- def lazyload(): - if self._should_log_debug: - self.logger.debug("lazy load attribute %s on instance %s" % (self.key, mapperutil.instance_str(instance))) </del><ins>+ localparent = mapper.object_mapper(instance) </ins><span class="cx"> </span><del>- if not mapper.has_identity(instance): - return None </del><ins>+ # adjust for the PropertyLoader associated with the instance + # not being our own PropertyLoader. This can occur when entity_name + # mappers are used to map different versions of the same PropertyLoader + # to the class. + prop = localparent.get_property(self.key) + if prop is not self.parent_property: + return prop._get_strategy(LazyLoader).setup_loader(instance) + + return LoadLazyAttribute(instance, self.key, options, path) </ins><span class="cx"> </span><del>- session = sessionlib.object_session(instance) - if session is None: - try: - session = mapper.object_mapper(instance).get_session() - except exceptions.InvalidRequestError: - raise exceptions.InvalidRequestError("Parent instance %s is not bound to a Session, and no contextual session is established; lazy load operation of attribute '%s' cannot proceed" % (instance.__class__, self.key)) - - # if we have a simple straight-primary key load, use mapper.get() - # to possibly save a DB round trip - q = session.query(self.mapper).autoflush(False) - if path: - q = q._with_current_path(path) - if self.use_get: - params = {} - for col, bind in self.lazybinds.iteritems(): - # use the "committed" (database) version to get query column values - params[bind.key] = self.parent._get_committed_attr_by_column(instance, col) - ident = [] - nonnulls = False - for primary_key in self.select_mapper.primary_key: - bind = self.lazyreverse[primary_key] - v = params[bind.key] - if v is not None: - nonnulls = True - ident.append(v) - if not nonnulls: - return None - if options: - q = q._conditional_options(*options) - return q.get(ident) - elif self.order_by is not False: - q = q.order_by(self.order_by) - elif self.secondary is not None and self.secondary.default_order_by() is not None: - q = q.order_by(self.secondary.default_order_by()) - - if options: - q = q._conditional_options(*options) - q = q.filter(self.lazy_clause(instance)) - - result = q.all() - if self.uselist: - return result - else: - if result: - return result[0] - else: - return None - - return lazyload - </del><span class="cx"> def create_row_processor(self, selectcontext, mapper, row): </span><span class="cx"> if not self.is_class_level or len(selectcontext.options): </span><span class="cx"> def new_execute(instance, row, ispostselect, **flags): </span><span class="lines">@@ -424,7 +357,7 @@ </span><span class="cx"> (primaryjoin, secondaryjoin, remote_side) = (prop.polymorphic_primaryjoin, prop.polymorphic_secondaryjoin, prop.remote_side) </span><span class="cx"> </span><span class="cx"> binds = {} </span><del>- reverse = {} </del><ins>+ equated_columns = {} </ins><span class="cx"> </span><span class="cx"> def should_bind(targetcol, othercol): </span><span class="cx"> if reverse_direction and not secondaryjoin: </span><span class="lines">@@ -437,20 +370,17 @@ </span><span class="cx"> return </span><span class="cx"> leftcol = binary.left </span><span class="cx"> rightcol = binary.right </span><del>- </del><ins>+ + equated_columns[rightcol] = leftcol + equated_columns[leftcol] = rightcol + </ins><span class="cx"> if should_bind(leftcol, rightcol): </span><del>- col = leftcol - binary.left = binds.setdefault(leftcol, - sql.bindparam(None, None, type_=binary.right.type)) - reverse[rightcol] = binds[col] </del><ins>+ binary.left = binds[leftcol] = sql.bindparam(None, None, type_=binary.right.type) </ins><span class="cx"> </span><span class="cx"> # the "left is not right" compare is to handle part of a join clause that is "table.c.col1==table.c.col1", </span><span class="cx"> # which can happen in rare cases (test/orm/relationships.py RelationTest2) </span><span class="cx"> if leftcol is not rightcol and should_bind(rightcol, leftcol): </span><del>- col = rightcol - binary.right = binds.setdefault(rightcol, - sql.bindparam(None, None, type_=binary.left.type)) - reverse[leftcol] = binds[col] </del><ins>+ binary.right = binds[rightcol] = sql.bindparam(None, None, type_=binary.left.type) </ins><span class="cx"> </span><span class="cx"> lazywhere = primaryjoin </span><span class="cx"> </span><span class="lines">@@ -461,12 +391,87 @@ </span><span class="cx"> if reverse_direction: </span><span class="cx"> secondaryjoin = visitors.traverse(secondaryjoin, clone=True, visit_binary=visit_binary) </span><span class="cx"> lazywhere = sql.and_(lazywhere, secondaryjoin) </span><del>- return (lazywhere, binds, reverse) </del><ins>+ return (lazywhere, binds, equated_columns) </ins><span class="cx"> _create_lazy_clause = classmethod(_create_lazy_clause) </span><span class="cx"> </span><span class="cx"> LazyLoader.logger = logging.class_logger(LazyLoader) </span><span class="cx"> </span><ins>+class LoadLazyAttribute(object): + """callable, serializable loader object used by LazyLoader""" </ins><span class="cx"> </span><ins>+ def __init__(self, instance, key, options, path): + self.instance = instance + self.key = key + self.options = options + self.path = path + + def __getstate__(self): + return {'instance':self.instance, 'key':self.key, 'options':self.options, 'path':serialize_path(self.path)} + + def __setstate__(self, state): + self.instance = state['instance'] + self.key = state['key'] + self.options= state['options'] + self.path = deserialize_path(state['path']) + + def __call__(self): + instance = self.instance + + if not mapper.has_identity(instance): + return None + + instance_mapper = mapper.object_mapper(instance) + prop = instance_mapper.get_property(self.key) + strategy = prop._get_strategy(LazyLoader) + + if strategy._should_log_debug: + strategy.logger.debug("lazy load attribute %s on instance %s" % (self.key, mapperutil.instance_str(instance))) + + session = sessionlib.object_session(instance) + if session is None: + try: + session = instance_mapper.get_session() + except exceptions.InvalidRequestError: + raise exceptions.InvalidRequestError("Parent instance %s is not bound to a Session, and no contextual session is established; lazy load operation of attribute '%s' cannot proceed" % (instance.__class__, self.key)) + + q = session.query(prop.mapper).autoflush(False) + if self.path: + q = q._with_current_path(self.path) + + # if we have a simple primary key load, use mapper.get() + # to possibly save a DB round trip + if strategy.use_get: + ident = [] + allnulls = True + for primary_key in prop.select_mapper.primary_key: + val = instance_mapper._get_committed_attr_by_column(instance, strategy.equated_columns[primary_key]) + allnulls = allnulls and val is None + ident.append(val) + if allnulls: + return None + if self.options: + q = q._conditional_options(*self.options) + return q.get(ident) + + if strategy.order_by is not False: + q = q.order_by(strategy.order_by) + elif strategy.secondary is not None and strategy.secondary.default_order_by() is not None: + q = q.order_by(strategy.secondary.default_order_by()) + + if self.options: + q = q._conditional_options(*self.options) + q = q.filter(strategy.lazy_clause(instance)) + + result = q.all() + if strategy.uselist: + return result + else: + if result: + return result[0] + else: + return None + + </ins><span class="cx"> class EagerLoader(AbstractRelationLoader): </span><span class="cx"> """Loads related objects inline with a parent query.""" </span><span class="cx"> </span><span class="lines">@@ -630,8 +635,7 @@ </span><span class="cx"> if self._should_log_debug: </span><span class="cx"> self.logger.debug("eager loader %s degrading to lazy loader" % str(self)) </span><span class="cx"> return self.parent_property._get_strategy(LazyLoader).create_row_processor(selectcontext, mapper, row) </span><del>- - </del><ins>+ </ins><span class="cx"> def __str__(self): </span><span class="cx"> return str(self.parent) + "." + self.key </span><span class="cx"> </span></span></pre></div> <a id="sqlalchemytrunklibsqlalchemyormutilpy"></a> <div class="modfile"><h4>Modified: sqlalchemy/trunk/lib/sqlalchemy/orm/util.py (3967 => 3968)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/trunk/lib/sqlalchemy/orm/util.py 2007-12-20 02:37:48 UTC (rev 3967) +++ sqlalchemy/trunk/lib/sqlalchemy/orm/util.py 2007-12-21 06:57:20 UTC (rev 3968) </span><span class="lines">@@ -284,9 +284,11 @@ </span><span class="cx"> </span><span class="cx"> def state_str(state): </span><span class="cx"> """Return a string describing an instance.""" </span><ins>+ if state is None: + return "None" + else: + return state.class_.__name__ + "@" + hex(id(state.obj())) </ins><span class="cx"> </span><del>- return state.class_.__name__ + "@" + hex(id(state.obj())) - </del><span class="cx"> def attribute_str(instance, attribute): </span><span class="cx"> return instance_str(instance) + "." + attribute </span><span class="cx"> </span></span></pre></div> <a id="sqlalchemytrunklibsqlalchemysqlexpressionpy"></a> <div class="modfile"><h4>Modified: sqlalchemy/trunk/lib/sqlalchemy/sql/expression.py (3967 => 3968)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/trunk/lib/sqlalchemy/sql/expression.py 2007-12-20 02:37:48 UTC (rev 3967) +++ sqlalchemy/trunk/lib/sqlalchemy/sql/expression.py 2007-12-21 06:57:20 UTC (rev 3968) </span><span class="lines">@@ -47,7 +47,6 @@ </span><span class="cx"> 'subquery', 'table', 'text', 'union', 'union_all', 'update', ] </span><span class="cx"> </span><span class="cx"> </span><del>-BIND_PARAMS = re.compile(r'(?<![:\w\x5c]):(\w+)(?!:)', re.UNICODE) </del><span class="cx"> </span><span class="cx"> def desc(column): </span><span class="cx"> """Return a descending ``ORDER BY`` clause element. </span><span class="lines">@@ -1795,6 +1794,8 @@ </span><span class="cx"> </span><span class="cx"> __visit_name__ = 'textclause' </span><span class="cx"> </span><ins>+ _bind_params_regex = re.compile(r'(?<![:\w\x5c]):(\w+)(?!:)', re.UNICODE) + </ins><span class="cx"> def __init__(self, text = "", bind=None, bindparams=None, typemap=None): </span><span class="cx"> self._bind = bind </span><span class="cx"> self.bindparams = {} </span><span class="lines">@@ -1809,7 +1810,7 @@ </span><span class="cx"> </span><span class="cx"> # scan the string and search for bind parameter names, add them </span><span class="cx"> # to the list of bindparams </span><del>- self.text = BIND_PARAMS.sub(repl, text) </del><ins>+ self.text = self._bind_params_regex.sub(repl, text) </ins><span class="cx"> if bindparams is not None: </span><span class="cx"> for b in bindparams: </span><span class="cx"> self.bindparams[b.key] = b </span></span></pre></div> <a id="sqlalchemytrunktestormalltestspy"></a> <div class="modfile"><h4>Modified: sqlalchemy/trunk/test/orm/alltests.py (3967 => 3968)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/trunk/test/orm/alltests.py 2007-12-20 02:37:48 UTC (rev 3967) +++ sqlalchemy/trunk/test/orm/alltests.py 2007-12-21 06:57:20 UTC (rev 3968) </span><span class="lines">@@ -26,6 +26,7 @@ </span><span class="cx"> 'orm.relationships', </span><span class="cx"> 'orm.association', </span><span class="cx"> 'orm.merge', </span><ins>+ 'orm.pickled', </ins><span class="cx"> 'orm.memusage', </span><span class="cx"> </span><span class="cx"> 'orm.cycles', </span></span></pre></div> <a id="sqlalchemytrunktestormattributespy"></a> <div class="modfile"><h4>Modified: sqlalchemy/trunk/test/orm/attributes.py (3967 => 3968)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/trunk/test/orm/attributes.py 2007-12-20 02:37:48 UTC (rev 3967) +++ sqlalchemy/trunk/test/orm/attributes.py 2007-12-21 06:57:20 UTC (rev 3968) </span><span class="lines">@@ -95,6 +95,65 @@ </span><span class="cx"> self.assert_(o4.mt2[0].a == 'abcde') </span><span class="cx"> self.assert_(o4.mt2[0].b is None) </span><span class="cx"> </span><ins>+ def test_deferred(self): + class Foo(object):pass + + data = {'a':'this is a', 'b':12} + def loader(instance, keys): + for k in keys: + instance.__dict__[k] = data[k] + return attributes.ATTR_WAS_SET + + attributes.register_class(Foo, deferred_scalar_loader=loader) + attributes.register_attribute(Foo, 'a', uselist=False, useobject=False) + attributes.register_attribute(Foo, 'b', uselist=False, useobject=False) + + f = Foo() + f._state.expire_attributes(None) + self.assertEquals(f.a, "this is a") + self.assertEquals(f.b, 12) + + f.a = "this is some new a" + f._state.expire_attributes(None) + self.assertEquals(f.a, "this is a") + self.assertEquals(f.b, 12) + + f._state.expire_attributes(None) + f.a = "this is another new a" + self.assertEquals(f.a, "this is another new a") + self.assertEquals(f.b, 12) + + f._state.expire_attributes(None) + self.assertEquals(f.a, "this is a") + self.assertEquals(f.b, 12) + + del f.a + self.assertEquals(f.a, None) + self.assertEquals(f.b, 12) + + f._state.commit_all() + self.assertEquals(f.a, None) + self.assertEquals(f.b, 12) + + def test_deferred_pickleable(self): + data = {'a':'this is a', 'b':12} + def loader(instance, keys): + for k in keys: + instance.__dict__[k] = data[k] + return attributes.ATTR_WAS_SET + + attributes.register_class(MyTest, deferred_scalar_loader=loader) + attributes.register_attribute(MyTest, 'a', uselist=False, useobject=False) + attributes.register_attribute(MyTest, 'b', uselist=False, useobject=False) + + m = MyTest() + m._state.expire_attributes(None) + assert 'a' not in m.__dict__ + m2 = pickle.loads(pickle.dumps(m)) + assert 'a' not in m2.__dict__ + self.assertEquals(m2.a, "this is a") + self.assertEquals(m2.b, 12) + </ins><span class="cx"> def test_list(self): </span><span class="cx"> class User(object):pass </span><span class="cx"> class Address(object):pass </span><span class="lines">@@ -860,7 +919,6 @@ </span><span class="cx"> self.assertEquals(attributes.get_history(f._state, 'bars'), ([bar4], [], [])) </span><span class="cx"> </span><span class="cx"> lazy_load = [bar1, bar2, bar3] </span><del>- f._state.trigger = lazyload(f) </del><span class="cx"> f._state.expire_attributes(['bars']) </span><span class="cx"> self.assertEquals(attributes.get_history(f._state, 'bars'), ([], [bar1, bar2, bar3], [])) </span... [truncated message content] |