# strategies.py # Copyright (C) 2005, 2006, 2007 Michael Bayer mike_mp@zzzcomputing.com # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """sqlalchemy.orm.interfaces.LoaderStrategy implementations, and related MapperOptions.""" from sqlalchemy import sql, util, exceptions, sql_util, logging from sqlalchemy.orm import mapper, attributes from sqlalchemy.orm.interfaces import LoaderStrategy, StrategizedOption, MapperOption, PropertyOption from sqlalchemy.orm import session as sessionlib from sqlalchemy.orm import util as mapperutil class ColumnLoader(LoaderStrategy): def init(self): super(ColumnLoader, self).init() self.columns = self.parent_property.columns self._should_log_debug = logging.is_debug_enabled(self.logger) self.is_composite = hasattr(self.parent_property, 'composite_class') def setup_query(self, context, parentclauses=None, **kwargs): for c in self.columns: if parentclauses is not None: context.statement.append_column(parentclauses.aliased_column(c)) else: context.statement.append_column(c) def init_class_attribute(self): self.is_class_level = True if self.is_composite: self._init_composite_attribute() else: self._init_scalar_attribute() def _init_composite_attribute(self): self.logger.info("register managed composite attribute %s on class %s" % (self.key, self.parent.class_.__name__)) def copy(obj): return self.parent_property.composite_class( *obj.__composite_values__()) def compare(a, b): for col, aprop, bprop in zip(self.columns, a.__composite_values__(), b.__composite_values__()): if not col.type.compare_values(aprop, bprop): return False else: return True sessionlib.attribute_manager.register_attribute(self.parent.class_, self.key, uselist=False, copy_function=copy, compare_function=compare, mutable_scalars=True, comparator=self.parent_property.comparator) def _init_scalar_attribute(self): self.logger.info("register managed attribute %s on class %s" % (self.key, self.parent.class_.__name__)) coltype = self.columns[0].type sessionlib.attribute_manager.register_attribute(self.parent.class_, self.key, uselist=False, copy_function=coltype.copy_value, compare_function=coltype.compare_values, mutable_scalars=self.columns[0].type.is_mutable(), comparator=self.parent_property.comparator) def create_row_processor(self, selectcontext, mapper, row): if self.is_composite: for c in self.columns: if c not in row: break else: def execute(instance, row, isnew, ispostselect=None, **flags): if isnew or ispostselect: if self._should_log_debug: self.logger.debug("populating %s with %s/%s..." % (mapperutil.attribute_str(instance, self.key), row.__class__.__name__, self.columns[0].key)) instance.__dict__[self.key] = self.parent_property.composite_class(*[row[c] for c in self.columns]) self.logger.debug("Returning active composite column fetcher for %s %s" % (mapper, self.key)) return (execute, None) elif self.columns[0] in row: def execute(instance, row, isnew, ispostselect=None, **flags): if isnew or ispostselect: if self._should_log_debug: self.logger.debug("populating %s with %s/%s" % (mapperutil.attribute_str(instance, self.key), row.__class__.__name__, self.columns[0].key)) instance.__dict__[self.key] = row[self.columns[0]] self.logger.debug("Returning active column fetcher for %s %s" % (mapper, self.key)) return (execute, None) # 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) if hosted_mapper.polymorphic_fetch == 'deferred': # 'deferred' polymorphic row fetcher, put a callable on the property. def execute(instance, row, isnew, **flags): if isnew: sessionlib.attribute_manager.init_instance_attribute(instance, self.key, callable_=self._get_deferred_inheritance_loader(instance, mapper, needs_tables)) self.logger.debug("Returning deferred column fetcher for %s %s" % (mapper, self.key)) return (execute, None) else: # immediate polymorphic row fetcher. no processing needed for this row. self.logger.debug("Returning no column fetcher for %s %s" % (mapper, self.key)) return (None, None) def _get_deferred_inheritance_loader(self, instance, mapper, needs_tables): def create_statement(): cond, param_names = mapper._deferred_inheritance_condition(needs_tables) statement = sql.select(needs_tables, cond, use_labels=True) params = {} for c in param_names: params[c.name] = mapper.get_attr_by_column(instance, c) return (statement, params) strategy = self.parent_property._get_strategy(DeferredColumnLoader) props = [p for p in mapper.iterate_properties if isinstance(p.strategy, ColumnLoader) and p.columns[0].table in needs_tables] return strategy.setup_loader(instance, props=props, create_statement=create_statement) ColumnLoader.logger = logging.class_logger(ColumnLoader) class DeferredColumnLoader(LoaderStrategy): """Describes an object attribute that corresponds to a table column, which also will *lazy load* its value from the table. This is per-column lazy loading. """ def create_row_processor(self, selectcontext, mapper, row): if self.group is not None and selectcontext.attributes.get(('undefer', self.group), False): return self.parent_property._get_strategy(ColumnLoader).create_row_processor(selectcontext, mapper, row) elif not self.is_class_level or len(selectcontext.options): def execute(instance, row, isnew, **flags): if isnew: if self._should_log_debug: self.logger.debug("set deferred callable on %s" % mapperutil.attribute_str(instance, self.key)) sessionlib.attribute_manager.init_instance_attribute(instance, self.key, callable_=self.setup_loader(instance)) return (execute, None) else: def execute(instance, row, isnew, **flags): if isnew: if self._should_log_debug: self.logger.debug("set deferred callable on %s" % mapperutil.attribute_str(instance, self.key)) sessionlib.attribute_manager.reset_instance_attribute(instance, self.key) return (execute, None) def init(self): super(DeferredColumnLoader, self).init() if hasattr(self.parent_property, 'composite_class'): raise NotImplementedError("Deferred loading for composite types not implemented yet") self.columns = self.parent_property.columns self.group = self.parent_property.group self._should_log_debug = logging.is_debug_enabled(self.logger) def init_class_attribute(self): self.is_class_level = True self.logger.info("register managed attribute %s on class %s" % (self.key, self.parent.class_.__name__)) sessionlib.attribute_manager.register_attribute(self.parent.class_, self.key, uselist=False, callable_=self.setup_loader, copy_function=self.columns[0].type.copy_value, compare_function=self.columns[0].type.compare_values, mutable_scalars=self.columns[0].type.is_mutable(), comparator=self.parent_property.comparator) def setup_query(self, context, **kwargs): if self.group is not None and context.attributes.get(('undefer', self.group), False): self.parent_property._get_strategy(ColumnLoader).setup_query(context, **kwargs) def setup_loader(self, instance, props=None, create_statement=None): localparent = mapper.object_mapper(instance, raiseerror=False) if localparent is None: return None # adjust for the ColumnProperty associated with the instance # not being our own ColumnProperty. This can occur when entity_name # mappers are used to map different versions of the same ColumnProperty # to the class. prop = localparent.get_property(self.key) if prop is not self.parent_property: return prop._get_strategy(DeferredColumnLoader).setup_loader(instance) 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 for p in localparent.iterate_properties if isinstance(p.strategy, DeferredColumnLoader) and p.group==self.group] else: group = [self.parent_property] if self._should_log_debug: self.logger.debug("deferred load %s group %s" % (mapperutil.attribute_str(instance, self.key), group and ','.join([p.key for p in group]) or 'None')) 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: clause = localparent._get_clause ident = instance._instance_key[1] params = {} for i, primary_key in enumerate(localparent.primary_key): params[primary_key._label] = ident[i] statement = sql.select([p.columns[0] for p in group], clause, from_obj=[localparent.mapped_table], use_labels=True) else: statement, params = create_statement() result = session.execute(statement, params, mapper=localparent) try: row = result.fetchone() for prop in group: sessionlib.attribute_manager.get_attribute(instance, prop.key).set_committed_value(instance, row[prop.columns[0]]) return attributes.ATTR_WAS_SET finally: result.close() return lazyload DeferredColumnLoader.logger = logging.class_logger(DeferredColumnLoader) class DeferredOption(StrategizedOption): def __init__(self, key, defer=False): super(DeferredOption, self).__init__(key) self.defer = defer def get_strategy_class(self): if self.defer: return DeferredColumnLoader else: return ColumnLoader class UndeferGroupOption(MapperOption): def __init__(self, group): self.group = group def process_query_context(self, context): context.attributes[('undefer', self.group)] = True def process_selection_context(self, context): context.attributes[('undefer', self.group)] = True class AbstractRelationLoader(LoaderStrategy): def init(self): super(AbstractRelationLoader, self).init() for attr in ['primaryjoin', 'secondaryjoin', 'secondary', 'foreign_keys', 'mapper', 'select_mapper', 'target', 'select_table', 'loads_polymorphic', 'uselist', 'cascade', 'attributeext', 'order_by', 'remote_side', 'polymorphic_primaryjoin', 'polymorphic_secondaryjoin', 'direction']: setattr(self, attr, getattr(self.parent_property, attr)) self._should_log_debug = logging.is_debug_enabled(self.logger) def _init_instance_attribute(self, instance, callable_=None): return sessionlib.attribute_manager.init_instance_attribute(instance, self.key, callable_=callable_) def _register_attribute(self, class_, callable_=None, **kwargs): self.logger.info("register managed %s attribute %s on class %s" % ((self.uselist and "list-holding" or "scalar"), self.key, self.parent.class_.__name__)) sessionlib.attribute_manager.register_attribute(class_, self.key, uselist = self.uselist, extension=self.attributeext, cascade=self.cascade, trackparent=True, typecallable=self.parent_property.collection_class, callable_=callable_, comparator=self.parent_property.comparator, **kwargs) class DynaLoader(AbstractRelationLoader): def init_class_attribute(self): self.is_class_level = True self._register_attribute(self.parent.class_, dynamic=True, target_mapper=self.parent_property.mapper) def create_row_processor(self, selectcontext, mapper, row): return (None, None) DynaLoader.logger = logging.class_logger(DynaLoader) class NoLoader(AbstractRelationLoader): def init_class_attribute(self): self.is_class_level = True self._register_attribute(self.parent.class_) def create_row_processor(self, selectcontext, mapper, row): def execute(instance, row, isnew, **flags): if isnew: if self._should_log_debug: self.logger.debug("initializing blank scalar/collection on %s" % mapperutil.attribute_str(instance, self.key)) self._init_instance_attribute(instance) return (execute, None) NoLoader.logger = logging.class_logger(NoLoader) class LazyLoader(AbstractRelationLoader): def init(self): super(LazyLoader, self).init() (self.lazywhere, self.lazybinds, self.lazyreverse) = self._create_lazy_clause(self) self.logger.info(str(self.parent_property) + " lazy loading clause " + str(self.lazywhere)) # determine if our "lazywhere" clause is the same as the mapper's # get() clause. then we can just use mapper.get() #from sqlalchemy.orm import query self.use_get = not self.uselist and self.mapper._get_clause.compare(self.lazywhere) if self.use_get: self.logger.info(str(self.parent_property) + " will use query.get() to optimize instance loads") def init_class_attribute(self): self.is_class_level = True self._register_attribute(self.parent.class_, callable_=lambda i: self.setup_loader(i)) def lazy_clause(self, instance, reverse_direction=False): if not reverse_direction: (criterion, lazybinds, rev) = (self.lazywhere, self.lazybinds, self.lazyreverse) else: (criterion, lazybinds, rev) = LazyLoader._create_lazy_clause(self.parent_property, reverse_direction=reverse_direction) bind_to_col = dict([(lazybinds[col].key, col) for col in lazybinds]) class Visitor(sql.ClauseVisitor): def visit_bindparam(s, bindparam): mapper = reverse_direction and self.parent_property.mapper or self.parent_property.parent if bindparam.key in bind_to_col: bindparam.value = mapper.get_attr_by_column(instance, bind_to_col[bindparam.key]) return Visitor().traverse(criterion, clone=True) def setup_loader(self, instance, options=None): if not mapper.has_mapper(instance): return None 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) def lazyload(): self.logger.debug("lazy load attribute %s on instance %s" % (self.key, mapperutil.instance_str(instance))) if not mapper.has_identity(instance): return None 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 self.use_get: params = {} for col, bind in self.lazybinds.iteritems(): params[bind.key] = self.parent.get_attr_by_column(instance, col) ident = [] for primary_key in self.select_mapper.primary_key: bind = self.lazyreverse[primary_key] ident.append(params[bind.key]) if options: q = q.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.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 def create_row_processor(self, selectcontext, mapper, row): if not self.is_class_level or len(selectcontext.options): def execute(instance, row, isnew, **flags): if isnew: if self._should_log_debug: self.logger.debug("set instance-level lazy loader on %s" % mapperutil.attribute_str(instance, self.key)) # we are not the primary manager for this attribute on this class - set up a per-instance lazyloader, # which will override the class-level behavior self._init_instance_attribute(instance, callable_=self.setup_loader(instance, selectcontext.options)) return (execute, None) else: def execute(instance, row, isnew, **flags): if isnew: if self._should_log_debug: self.logger.debug("set class-level lazy loader on %s" % mapperutil.attribute_str(instance, self.key)) # we are the primary manager for this attribute on this class - reset its per-instance attribute state, # so that the class-level lazy loader is executed when next referenced on this instance. # this usually is not needed unless the constructor of the object referenced the attribute before we got # to load data into it. sessionlib.attribute_manager.reset_instance_attribute(instance, self.key) return (execute, None) def _create_lazy_clause(cls, prop, reverse_direction=False): (primaryjoin, secondaryjoin, remote_side) = (prop.polymorphic_primaryjoin, prop.polymorphic_secondaryjoin, prop.remote_side) binds = {} reverse = {} def should_bind(targetcol, othercol): if reverse_direction and not secondaryjoin: return targetcol in remote_side else: return othercol in remote_side def find_column_in_expr(expr): if not isinstance(expr, sql.ColumnElement): return None columns = [] class FindColumnInColumnClause(sql.ClauseVisitor): def visit_column(self, c): columns.append(c) FindColumnInColumnClause().traverse(expr) return len(columns) and columns[0] or None def visit_binary(binary): leftcol = find_column_in_expr(binary.left) rightcol = find_column_in_expr(binary.right) if leftcol is None or rightcol is None: return if should_bind(leftcol, rightcol): col = leftcol binary.left = binds.setdefault(leftcol, sql.bindparam(None, None, shortname=leftcol.name, type_=binary.right.type, unique=True)) reverse[rightcol] = binds[col] # the "left is not right" compare is to handle part of a join clause that is "table.c.col1==table.c.col1", # which can happen in rare cases (test/orm/relationships.py RelationTest2) if leftcol is not rightcol and should_bind(rightcol, leftcol): col = rightcol binary.right = binds.setdefault(rightcol, sql.bindparam(None, None, shortname=rightcol.name, type_=binary.left.type, unique=True)) reverse[leftcol] = binds[col] lazywhere = primaryjoin li = mapperutil.BinaryVisitor(visit_binary) if not secondaryjoin or not reverse_direction: lazywhere = li.traverse(lazywhere, clone=True) if secondaryjoin is not None: if reverse_direction: secondaryjoin = li.traverse(secondaryjoin, clone=True) lazywhere = sql.and_(lazywhere, secondaryjoin) return (lazywhere, binds, reverse) _create_lazy_clause = classmethod(_create_lazy_clause) LazyLoader.logger = logging.class_logger(LazyLoader) class EagerLoader(AbstractRelationLoader): """Loads related objects inline with a parent query.""" def init(self): super(EagerLoader, self).init() self.clauses = {} self.join_depth = self.parent_property.join_depth def init_class_attribute(self): # class-level eager strategy; add the PropertyLoader # to the parent's list of "eager loaders"; this tells the Query # that eager loaders will be used in a normal query self.parent._eager_loaders.add(self.parent_property) # initialize a lazy loader on the class level attribute self.parent_property._get_strategy(LazyLoader).init_class_attribute() def setup_query(self, context, parentclauses=None, parentmapper=None, **kwargs): """Add a left outer join to the statement thats being constructed.""" # build a path as we setup the query. the format of this path # matches that of interfaces.LoaderStack, and will be used in the # row-loading phase to match up AliasedClause objects with the current # LoaderStack position. if parentclauses: path = parentclauses.path + (self.parent.base_mapper, self.key) else: path = (self.parent.base_mapper, self.key) if self.join_depth: if len(path) / 2 > self.join_depth: return else: if self.mapper in path: return #print "CREATING EAGER PATH FOR", "->".join([str(s) for s in path]) if parentmapper is None: localparent = context.mapper else: localparent = parentmapper statement = context.statement if hasattr(statement, '_outerjoin'): towrap = statement._outerjoin elif isinstance(localparent.mapped_table, sql.Join): towrap = localparent.mapped_table else: # look for the mapper's selectable expressed within the current "from" criterion. # this will locate the selectable inside of any containers it may be a part of (such # as a join). if its inside of a join, we want to outer join on that join, not the # selectable. for fromclause in statement.froms: if fromclause is localparent.mapped_table: towrap = fromclause break elif isinstance(fromclause, sql.Join): if localparent.mapped_table in sql_util.TableFinder(fromclause, include_aliases=True): towrap = fromclause break else: raise exceptions.InvalidRequestError("EagerLoader cannot locate a clause with which to outer join to, in query '%s' %s" % (str(statement), localparent.mapped_table)) try: clauses = self.clauses[path] except KeyError: clauses = mapperutil.PropertyAliasedClauses(self.parent_property, self.parent_property.polymorphic_primaryjoin, self.parent_property.polymorphic_secondaryjoin, parentclauses) self.clauses[path] = clauses if self.secondaryjoin is not None: statement._outerjoin = sql.outerjoin(towrap, clauses.secondary, clauses.primaryjoin).outerjoin(clauses.alias, clauses.secondaryjoin) if self.order_by is False and self.secondary.default_order_by() is not None: statement.append_order_by(*clauses.secondary.default_order_by()) else: statement._outerjoin = towrap.outerjoin(clauses.alias, clauses.primaryjoin) if self.order_by is False and clauses.alias.default_order_by() is not None: statement.append_order_by(*clauses.alias.default_order_by()) if clauses.order_by: statement.append_order_by(*util.to_list(clauses.order_by)) statement.append_from(statement._outerjoin) for value in self.select_mapper.iterate_properties: value.setup(context, parentclauses=clauses, parentmapper=self.select_mapper) def _create_row_decorator(self, selectcontext, row, path): """Create a *row decorating* function that will apply eager aliasing to the row. Also check that an identity key can be retrieved from the row, else return None. """ #print "creating row decorator for path ", "->".join([str(s) for s in path]) # check for a user-defined decorator in the SelectContext (which was set up by the contains_eager() option) if ("eager_row_processor", self.parent_property) in selectcontext.attributes: # custom row decoration function, placed in the selectcontext by the # contains_eager() mapper option decorator = selectcontext.attributes[("eager_row_processor", self.parent_property)] if decorator is None: decorator = lambda row: row else: try: # decorate the row according to the stored AliasedClauses for this eager load clauses = self.clauses[path] decorator = clauses.row_decorator except KeyError, k: # no stored AliasedClauses: eager loading was not set up in the query and # AliasedClauses never got initialized if self._should_log_debug: self.logger.debug("Could not locate aliased clauses for key: " + str(path)) return None try: decorated_row = decorator(row) # check for identity key identity_key = self.select_mapper.identity_key_from_row(decorated_row) # and its good return decorator except KeyError, k: # no identity key - dont return a row processor, will cause a degrade to lazy if self._should_log_debug: self.logger.debug("could not locate identity key from row '%s'; missing column '%s'" % (repr(decorated_row), str(k))) return None def create_row_processor(self, selectcontext, mapper, row): selectcontext.stack.push_property(self.key) path = selectcontext.stack.snapshot() row_decorator = self._create_row_decorator(selectcontext, row, path) if row_decorator is not None: def execute(instance, row, isnew, **flags): decorated_row = row_decorator(row) selectcontext.stack.push_property(self.key) if not self.uselist: if self._should_log_debug: self.logger.debug("eagerload scalar instance on %s" % mapperutil.attribute_str(instance, self.key)) if isnew: # set a scalar object instance directly on the # parent object, bypassing InstrumentedAttribute # event handlers. # # FIXME: instead of... sessionlib.attribute_manager.get_attribute(instance, self.key).set_raw_value(instance, self.select_mapper._instance(selectcontext, decorated_row, None)) # bypass and set directly: #instance.__dict__[self.key] = ... else: # call _instance on the row, even though the object has been created, # so that we further descend into properties self.select_mapper._instance(selectcontext, decorated_row, None) else: if isnew: if self._should_log_debug: self.logger.debug("initialize UniqueAppender on %s" % mapperutil.attribute_str(instance, self.key)) collection = sessionlib.attribute_manager.init_collection(instance, self.key) appender = util.UniqueAppender(collection, 'append_without_event') # store it in the "scratch" area, which is local to this load operation. selectcontext.attributes[(instance, self.key)] = appender result_list = selectcontext.attributes[(instance, self.key)] if self._should_log_debug: self.logger.debug("eagerload list instance on %s" % mapperutil.attribute_str(instance, self.key)) self.select_mapper._instance(selectcontext, decorated_row, result_list) selectcontext.stack.pop() selectcontext.stack.pop() return (execute, None) else: self.logger.debug("eager loader %s degrading to lazy loader" % str(self)) selectcontext.stack.pop() return self.parent_property._get_strategy(LazyLoader).create_row_processor(selectcontext, mapper, row) def __str__(self): return str(self.parent) + "." + self.key EagerLoader.logger = logging.class_logger(EagerLoader) class EagerLazyOption(StrategizedOption): def __init__(self, key, lazy=True, chained=False): super(EagerLazyOption, self).__init__(key) self.lazy = lazy self.chained = chained def is_chained(self): return not self.lazy and self.chained def process_query_property(self, context, properties): if self.lazy: if properties[-1] in context.eager_loaders: context.eager_loaders.remove(properties[-1]) else: for prop in properties: context.eager_loaders.add(prop) super(EagerLazyOption, self).process_query_property(context, properties) def get_strategy_class(self): if self.lazy: return LazyLoader elif self.lazy is False: return EagerLoader elif self.lazy is None: return NoLoader EagerLazyOption.logger = logging.class_logger(EagerLazyOption) # TODO: enable FetchMode option. currently # this class does nothing. will require Query # to swich between using its "polymorphic" selectable # and its regular selectable in order to make decisions # (therefore might require that FetchModeOperation is performed # only as the first operation on a Query.) class FetchModeOption(PropertyOption): def __init__(self, key, type): super(FetchModeOption, self).__init__(key) if type not in ('join', 'select'): raise exceptions.ArgumentError("Fetchmode must be one of 'join' or 'select'") self.type = type def process_selection_property(self, context, properties): context.attributes[('fetchmode', properties[-1])] = self.type class RowDecorateOption(PropertyOption): def __init__(self, key, decorator=None, alias=None): super(RowDecorateOption, self).__init__(key) self.decorator = decorator self.alias = alias def process_selection_property(self, context, properties): if self.alias is not None and self.decorator is None: if isinstance(self.alias, basestring): self.alias = properties[-1].target.alias(self.alias) def decorate(row): d = {} for c in properties[-1].target.columns: d[c] = row[self.alias.corresponding_column(c)] return d self.decorator = decorate context.attributes[("eager_row_processor", properties[-1])] = self.decorator RowDecorateOption.logger = logging.class_logger(RowDecorateOption)