From 8e297afa8ddd66d5d7317a1b965a8919adb8f1aa Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 30 May 2023 14:54:42 -0400 Subject: [PATCH] use internal declarative creator for DeclarativeBaseNoMeta Fixed issue where :class:`.DeclarativeBaseNoMeta` declarative base class would not function with non-mapped mixins or abstract classes, raising an ``AttributeError`` instead. Fixes: #9862 Change-Id: I91cfe663530a2eb712004b9fb09d3f0cefcaeef5 --- doc/build/changelog/unreleased_20/9862.rst | 7 ++++ lib/sqlalchemy/orm/decl_api.py | 2 +- test/orm/declarative/test_basic.py | 48 ++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 doc/build/changelog/unreleased_20/9862.rst diff --git a/doc/build/changelog/unreleased_20/9862.rst b/doc/build/changelog/unreleased_20/9862.rst new file mode 100644 index 0000000000..efa6274a98 --- /dev/null +++ b/doc/build/changelog/unreleased_20/9862.rst @@ -0,0 +1,7 @@ +.. change:: + :tags: bug, orm + :tickets: 9862 + + Fixed issue where :class:`.DeclarativeBaseNoMeta` declarative base class + would not function with non-mapped mixins or abstract classes, raising an + ``AttributeError`` instead. diff --git a/lib/sqlalchemy/orm/decl_api.py b/lib/sqlalchemy/orm/decl_api.py index cb25e34ca0..01b78aa99c 100644 --- a/lib/sqlalchemy/orm/decl_api.py +++ b/lib/sqlalchemy/orm/decl_api.py @@ -943,7 +943,7 @@ class DeclarativeBaseNoMeta(inspection.Inspectable[InstanceState[Any]]): _check_not_declarative(cls, DeclarativeBaseNoMeta) _setup_declarative_base(cls) else: - cls._sa_registry.map_declaratively(cls) + _as_declarative(cls._sa_registry, cls, cls.__dict__) def add_mapped_attribute( diff --git a/test/orm/declarative/test_basic.py b/test/orm/declarative/test_basic.py index 3ad7db0799..a76e375b22 100644 --- a/test/orm/declarative/test_basic.py +++ b/test/orm/declarative/test_basic.py @@ -1151,6 +1151,54 @@ class DeclarativeMultiBaseTest( eq_(a1, Address(email="two")) eq_(a1.user, User(name="u1")) + @testing.variation("mora", ["mixin", "abstract"]) + def test_abstract_and_or_mixin(self, mora): + if mora.abstract: + + class Employee(Base): + __abstract__ = True + + id = mapped_column(Integer, primary_key=True, sort_order=-1) + + class Manager(Employee): + __tablename__ = "manager" + name = mapped_column(String(50)) + manager_data = mapped_column(String(40)) + + class Engineer(Employee): + __tablename__ = "engineer" + + name = mapped_column(String(50)) + engineer_info = mapped_column(String(40)) + + elif mora.mixin: + + class Mixin: + pass + + class EmployeeMixin: + id = mapped_column(Integer, primary_key=True, sort_order=-1) + + class Manager(EmployeeMixin, Base): + __tablename__ = "manager" + name = mapped_column(String(50)) + manager_data = mapped_column(String(40)) + + class Engineer(EmployeeMixin, Base): + __tablename__ = "engineer" + + name = mapped_column(String(50)) + engineer_info = mapped_column(String(40)) + + else: + mora.fail() + + self.assert_compile( + select(Engineer), + "SELECT engineer.id, engineer.name, engineer.engineer_info " + "FROM engineer", + ) + def test_back_populates_setup(self): class User(Base): __tablename__ = "users" -- 2.39.5