From 9d12d493eb38f958c2d50da28f83ccc6de01f0dc Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 28 Jun 2022 18:55:19 -0400 Subject: produce column copies up the whole hierarchy first Fixed issue where a hierarchy of classes set up as an abstract or mixin declarative classes could not declare standalone columns on a superclass that would then be copied correctly to a :class:`_orm.declared_attr` callable that wanted to make use of them on a descendant class. Originally it looked like this would produce an ordering change, however an adjustment to the flow for produce_column_copies has avoided that for now. Fixes: #8190 Change-Id: I4e2ee74edb110793eb42691c3e4a0e0535fba7e9 --- lib/sqlalchemy/orm/decl_base.py | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/decl_base.py b/lib/sqlalchemy/orm/decl_base.py index 1366bedf2..62251fa2b 100644 --- a/lib/sqlalchemy/orm/decl_base.py +++ b/lib/sqlalchemy/orm/decl_base.py @@ -714,7 +714,14 @@ class _ClassScanMapperConfig(_MapperConfig): attribute_is_overridden = self._cls_attr_override_checker(self.cls) + bases = [] + for base in cls.__mro__: + # collect bases and make sure standalone columns are copied + # to be the column they will ultimately be on the class, + # so that declared_attr functions use the right columns. + # need to do this all the way up the hierarchy first + # (see #8190) class_mapped = ( base is not cls @@ -727,10 +734,34 @@ class _ClassScanMapperConfig(_MapperConfig): local_attributes_for_class = self._cls_attr_resolver(base) if not class_mapped and base is not cls: - self._produce_column_copies( + locally_collected_columns = self._produce_column_copies( local_attributes_for_class, attribute_is_overridden, ) + else: + locally_collected_columns = {} + + bases.append( + ( + base, + class_mapped, + local_attributes_for_class, + locally_collected_columns, + ) + ) + + for ( + base, + class_mapped, + local_attributes_for_class, + locally_collected_columns, + ) in bases: + + # this transfer can also take place as we scan each name + # for finer-grained control of how collected_attributes is + # populated, as this is what impacts column ordering. + # however it's simpler to get it out of the way here. + collected_attributes.update(locally_collected_columns) for ( name, @@ -738,6 +769,7 @@ class _ClassScanMapperConfig(_MapperConfig): annotation, is_dataclass_field, ) in local_attributes_for_class(): + if re.match(r"^__.+__$", name): if name == "__mapper_args__": check_decl = _check_declared_props_nocascade( @@ -1096,10 +1128,10 @@ class _ClassScanMapperConfig(_MapperConfig): [], Iterable[Tuple[str, Any, Any, bool]] ], attribute_is_overridden: Callable[[str, Any], bool], - ) -> None: + ) -> Dict[str, Union[Column[Any], MappedColumn[Any]]]: cls = self.cls dict_ = self.clsdict_view - collected_attributes = self.collected_attributes + locally_collected_attributes = {} column_copies = self.column_copies # copy mixin columns to the mapped class @@ -1132,9 +1164,10 @@ class _ClassScanMapperConfig(_MapperConfig): ) column_copies[obj] = copy_ = obj._copy() - collected_attributes[name] = copy_ + locally_collected_attributes[name] = copy_ setattr(cls, name, copy_) + return locally_collected_attributes def _extract_mappable_attributes(self) -> None: cls = self.cls -- cgit v1.2.1