Visitar URL original
Fix __allow_unmapped__ not honored for Column/MappedColumn on mixins by Faseeh06 · Pull Request #13532 · sqlalchemy/sqlalchemy · GitHub
Skip to content

Fix __allow_unmapped__ not honored for Column/MappedColumn on mixins - #13532

Open
Faseeh06 wants to merge 1 commit into
sqlalchemy:mainfrom
Faseeh06:fix/allow-unmapped-mixin-columns
Open

Faseeh06 wants to merge 1 commit into
sqlalchemy:mainfrom
Faseeh06:fix/allow-unmapped-mixin-columns

Conversation

@Faseeh06

Copy link
Copy Markdown

Fixes #9369

_produce_column_copies(), used when a Column or MappedColumn is inherited from a mixin/non-declarative base and copied onto the class being mapped, calls _collect_annotation() with expect_mapped hardcoded to True at both of its call sites. This bypasses self.allow_unmapped_annotations entirely, so a bare (non-Mapped[]) type-annotated attribute paired with such a column — inherited from a mixin — raises MappedAnnotationError even when __allow_unmapped__ = True is set on the mapped class or a superclass, despite the error's own text claiming that flag should suppress it:

class Base(DeclarativeBase):
    __allow_unmapped__ = True

class Mixin:
    id: int = Column(Integer, primary_key=True)

class MyModel(Mixin, Base):
    __tablename__ = "my_model"
    # currently raises MappedAnnotationError despite __allow_unmapped__

By contrast, the equivalent path for attributes declared directly on the mapped class passes expect_mapped=None, which correctly defers to the flag via the dynamic computation already present in _collect_annotation.

Changes

  • Pass not self.allow_unmapped_annotations instead of a hardcoded True at both _produce_column_copies() call sites, preserving the strict default while honoring the flag, matching the direct-attribute path's behavior.
  • Added a regression test (test_allow_unmapped_column_on_mixin) alongside the existing test_allow_unmapped_on_mixin/test_allow_unmapped_cols tests.
  • Added a changelog entry.

Test plan

  • Reproduced the exact repro from the issue against current main, confirmed fixed
  • Confirmed the strict default (no __allow_unmapped__) still correctly raises MappedAnnotationError for the same mixin shape — no over-broadening
  • New test fails on unmodified main and passes with the fix
  • pytest test/orm/declarative/ — 2852 passed
  • pytest test/orm/ (full ORM suite) — 10937 passed
  • flake8 / black --check clean on both changed files

_produce_column_copies(), used when a Column or MappedColumn is
inherited from a mixin/non-declarative base and copied onto the
mapped class, called _collect_annotation() with expect_mapped
hardcoded to True at both of its call sites. This bypassed
self.allow_unmapped_annotations entirely, so a bare (non-Mapped[])
annotation paired with such a column raised MappedAnnotationError
even when __allow_unmapped__ = True was set - despite the error's
own text claiming that flag should suppress it.

By contrast, the equivalent path for attributes declared directly
on the mapped class passes expect_mapped=None, correctly deferring
to the flag via the dynamic computation in _collect_annotation.

Pass "not self.allow_unmapped_annotations" instead of a hardcoded
True at both call sites, so the strict default is preserved but
the flag is now honored, matching the direct-attribute path.
@sqla-tester

Copy link
Copy Markdown
Collaborator

Hi, and thanks for the pull request!

This project accepts pull requests only for issues that a maintainer has already marked with the open for pull requests label. That way we can settle on an approach before anyone spends time writing code.

This pull request references issue #9369, which hasn't been marked open for pull requests, so the change isn't authorized yet and I'm closing it automatically. Once a maintainer adds the label to #9369, reopen this pull request and it will stay open. If #9369 needs more detail before that can happen, a complete runnable example is usually the missing piece, and adding one to the issue is the fastest way to get there.

This is automatic and procedural. It isn't a judgment on your change, and nothing you've written here is lost.

Thanks for your interest in the project!

@CaselIT CaselIT reopened this Aug 24, 2026
@Mukller

Mukller commented Aug 24, 2026

Copy link
Copy Markdown

Traced this through both functions to be sure, and the two-line fix is semantically exact:

  • With expect_mapped=False (i.e. __allow_unmapped__ = True), a bare annotation takes the return annotated, None branch in _extract_mapped_subtype (orm/util.py) instead of raising — the column then copies over with its plain type, exactly what allow_unmapped seems to not be honored in all cases #9369 asks for.
  • A Mapped[...] annotation on a mixin-inherited Column/MappedColumn is unaffected: the raise-block only triggers when the annotation's __origin__ isn't _MappedAnnotationBase, so extraction proceeds identically under either expect_mapped value.
  • With the flag off, expect_mapped=True reproduces the old behavior bit-for-bit, so non-mixin / strict-mode paths see no change.

The regression test targets the right path (mixin + explicit Column + bare annotation goes through _produce_column_copies, unlike the adjacent non-Column test), and asserting on __table__.c.keys() verifies both attributes survived rather than just absence-of-exception.

Nothing else to flag — clean fix for a long-standing gap.

name,
annotation,
originating_class,
not self.allow_unmapped_annotations,

@CaselIT CaselIT Aug 24, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this and below could instead pass None.

@zzzeek thoughts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in other places for such a case we pass none not False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

he's correct. use None please, instead of True

also we need to document what "allow_unmapped" means, determine if False means anything and if not we need to change this parameter

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what im getting at is this is a bigger change that warrants a lot of care and caution. it's not a one liner. especially in the tests. needs a lot more and they need to be integrated with what we have already.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zzzeek Should I check it at broader level and then let you know ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you want, sure

def test_allow_unmapped_column_on_mixin(self, decl_base):
"""test #9369"""

class Mixin:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not what the test case in the issue says....the test case has the __allow_unmapped__ on the Mixin.

in fact we should test it in all combinations: on just the mixin, on just the concrete class, on both. use testing.variations


class MyClass(Mixin, decl_base):
__tablename__ = "mytable"
__allow_unmapped__ = True

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file will also need to be copied using tools/sync_test_files.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

allow_unmapped seems to not be honored in all cases

6 participants