From: Mike Bayer Date: Fri, 20 Oct 2023 14:19:35 +0000 (-0400) Subject: run declarative scan for non-mapped annotated if allow_unmapped X-Git-Tag: rel_2_0_23~17^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f933d8bb6b6f852d700cdef8dda5d0edff7c148f;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git run declarative scan for non-mapped annotated if allow_unmapped Fixed issue where the ``__allow_unmapped__`` directive failed to allow for legacy :class:`.Column` / :func:`.deferred` mappings that nonetheless had annotations such as ``Any`` or a specific type without ``Mapped[]`` as their type, without errors related to locating the attribute name. issue is not ticketed yet but is coming Fixes: #10516 Change-Id: I471d6838f4dcc113addd284a39a5bb0b885b64ea --- diff --git a/doc/build/changelog/unreleased_20/10516.rst b/doc/build/changelog/unreleased_20/10516.rst new file mode 100644 index 0000000000..fadd12cadc --- /dev/null +++ b/doc/build/changelog/unreleased_20/10516.rst @@ -0,0 +1,8 @@ +.. change:: + :tags: bug, orm + :tickets: 10516 + + Fixed issue where the ``__allow_unmapped__`` directive failed to allow for + legacy :class:`.Column` / :func:`.deferred` mappings that nonetheless had + annotations such as ``Any`` or a specific type without ``Mapped[]`` as + their type, without errors related to locating the attribute name. diff --git a/lib/sqlalchemy/orm/decl_base.py b/lib/sqlalchemy/orm/decl_base.py index 9d10599499..d5ef3db470 100644 --- a/lib/sqlalchemy/orm/decl_base.py +++ b/lib/sqlalchemy/orm/decl_base.py @@ -1434,9 +1434,9 @@ class _ClassScanMapperConfig(_MapperConfig): cls, "_sa_decl_prepare_nocascade", strict=True ) + allow_unmapped_annotations = self.allow_unmapped_annotations expect_annotations_wo_mapped = ( - self.allow_unmapped_annotations - or self.is_dataclass_prior_to_mapping + allow_unmapped_annotations or self.is_dataclass_prior_to_mapping ) look_for_dataclass_things = bool(self.dataclass_setup_arguments) @@ -1531,7 +1531,15 @@ class _ClassScanMapperConfig(_MapperConfig): # Mapped[] etc. were not used. If annotation is None, # do declarative_scan so that the property can raise # for required - if mapped_container is not None or annotation is None: + if ( + mapped_container is not None + or annotation is None + # issue #10516: need to do declarative_scan even with + # a non-Mapped annotation if we are doing + # __allow_unmapped__, for things like col.name + # assignment + or allow_unmapped_annotations + ): try: value.declarative_scan( self, diff --git a/test/orm/declarative/test_tm_future_annotations_sync.py b/test/orm/declarative/test_tm_future_annotations_sync.py index 03c758352f..ec5f5e8209 100644 --- a/test/orm/declarative/test_tm_future_annotations_sync.py +++ b/test/orm/declarative/test_tm_future_annotations_sync.py @@ -373,6 +373,45 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL): status: int + @testing.variation("annotation", ["none", "any", "datatype"]) + @testing.variation("explicit_name", [True, False]) + @testing.variation("attribute", ["column", "deferred"]) + def test_allow_unmapped_cols(self, annotation, explicit_name, attribute): + class Base(DeclarativeBase): + __allow_unmapped__ = True + + if attribute.column: + if explicit_name: + attr = Column("data_one", Integer) + else: + attr = Column(Integer) + elif attribute.deferred: + if explicit_name: + attr = deferred(Column("data_one", Integer)) + else: + attr = deferred(Column(Integer)) + else: + attribute.fail() + + class MyClass(Base): + __tablename__ = "mytable" + + id: Mapped[int] = mapped_column(primary_key=True) + + if annotation.none: + data = attr + elif annotation.any: + data: Any = attr + elif annotation.datatype: + data: int = attr + else: + annotation.fail() + + if explicit_name: + eq_(MyClass.__table__.c.keys(), ["id", "data_one"]) + else: + eq_(MyClass.__table__.c.keys(), ["id", "data"]) + def test_column_default(self, decl_base): class MyClass(decl_base): __tablename__ = "mytable" diff --git a/test/orm/declarative/test_typed_mapping.py b/test/orm/declarative/test_typed_mapping.py index 4202166fbc..6b8becf9c0 100644 --- a/test/orm/declarative/test_typed_mapping.py +++ b/test/orm/declarative/test_typed_mapping.py @@ -364,6 +364,45 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL): status: int + @testing.variation("annotation", ["none", "any", "datatype"]) + @testing.variation("explicit_name", [True, False]) + @testing.variation("attribute", ["column", "deferred"]) + def test_allow_unmapped_cols(self, annotation, explicit_name, attribute): + class Base(DeclarativeBase): + __allow_unmapped__ = True + + if attribute.column: + if explicit_name: + attr = Column("data_one", Integer) + else: + attr = Column(Integer) + elif attribute.deferred: + if explicit_name: + attr = deferred(Column("data_one", Integer)) + else: + attr = deferred(Column(Integer)) + else: + attribute.fail() + + class MyClass(Base): + __tablename__ = "mytable" + + id: Mapped[int] = mapped_column(primary_key=True) + + if annotation.none: + data = attr + elif annotation.any: + data: Any = attr + elif annotation.datatype: + data: int = attr + else: + annotation.fail() + + if explicit_name: + eq_(MyClass.__table__.c.keys(), ["id", "data_one"]) + else: + eq_(MyClass.__table__.c.keys(), ["id", "data"]) + def test_column_default(self, decl_base): class MyClass(decl_base): __tablename__ = "mytable"