Fixed issue in Python 3.14 where dataclass transformation would fail when
a mapped class using :class:`.MappedAsDataclass` included a
:func:`.relationship` referencing a class that was not available at
runtime (e.g., within a ``TYPE_CHECKING`` block). This occurred when using
Python 3.14's :pep:`649` deferred annotations feature, which is the
default behavior without a ``from __future__ import annotations``
directive.
Fixes: #12952
Change-Id: I32f0adba00c32f5bf98fe2880dda1b96a7774d07
(cherry picked from commit
03dbd830e174685964191f739ea784f51c97d6b3)
--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 12952
+
+ Fixed issue in Python 3.14 where dataclass transformation would fail when
+ a mapped class using :class:`.MappedAsDataclass` included a
+ :func:`.relationship` referencing a class that was not available at
+ runtime (e.g., within a ``TYPE_CHECKING`` block). This occurred when using
+ Python 3.14's :pep:`649` deferred annotations feature, which is the
+ default behavior without a ``from __future__ import annotations``
+ directive.
# dataclasses callable, based on the fields present. This
# means remove the Mapped[] container and ensure all Field
# entries have an annotation
- restored = getattr(klass, "__annotations__", None)
+ restored = util.get_annotations(klass)
klass.__annotations__ = cast("Dict[str, Any]", use_annotations)
else:
restored = None
if restored is None:
del klass.__annotations__
else:
- klass.__annotations__ = restored
+ klass.__annotations__ = restored # type: ignore[assignment] # noqa: E501
@classmethod
def _assert_dc_arguments(cls, arguments: _DataclassArguments) -> None:
lambda: util.py312, "Python 3.12 or above required"
)
+ @property
+ def python314(self):
+ return exclusions.only_if(
+ lambda: util.py314, "Python 3.14 or above required"
+ )
+
@property
def fail_python314b1(self):
return exclusions.fails_if(
eq_(fields["id"].metadata, {})
eq_(fields["value"].metadata, {"meta_key": "meta_value"})
+ @testing.requires.python314
+ def test_apply_dc_deferred_annotations(self, dc_decl_base):
+ """test for #12952"""
+
+ class Message(dc_decl_base):
+ __tablename__ = "message"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ content: Mapped[str]
+ user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
+
+ # annotation is unquoted and refers to nonexistent class (and if
+ # this is test_dc_transforms.py, __future__ annotations is not
+ # turned on), so would be rejected by any python interpreter < 3.14
+ # up front. with python 3.14, the dataclass scan takes place
+ # and has to fetch the annotations using get_annotations()
+ # so that refs are turned into FwdRef without being resolved
+ user: Mapped[UnavailableUser] = relationship( # type: ignore # noqa
+ back_populates="messages"
+ )
+
+ # The key assertion: Message should be a dataclass
+ is_true(dataclasses.is_dataclass(Message))
+
+ # Verify the dataclass has proper __init__ signature
+ sig = pyinspect.signature(Message.__init__)
+ is_true("id" in sig.parameters)
+ is_true("content" in sig.parameters)
+ is_true("user_id" in sig.parameters)
+
class RelationshipDefaultFactoryTest(fixtures.TestBase):
def test_list(self, dc_decl_base: Type[MappedAsDataclass]):
eq_(fields["id"].metadata, {})
eq_(fields["value"].metadata, {"meta_key": "meta_value"})
+ @testing.requires.python314
+ def test_apply_dc_deferred_annotations(self, dc_decl_base):
+ """test for #12952"""
+
+ class Message(dc_decl_base):
+ __tablename__ = "message"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ content: Mapped[str]
+ user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
+
+ # annotation is unquoted and refers to nonexistent class (and if
+ # this is test_dc_transforms.py, __future__ annotations is not
+ # turned on), so would be rejected by any python interpreter < 3.14
+ # up front. with python 3.14, the dataclass scan takes place
+ # and has to fetch the annotations using get_annotations()
+ # so that refs are turned into FwdRef without being resolved
+ user: Mapped[UnavailableUser] = relationship( # type: ignore # noqa
+ back_populates="messages"
+ )
+
+ # The key assertion: Message should be a dataclass
+ is_true(dataclasses.is_dataclass(Message))
+
+ # Verify the dataclass has proper __init__ signature
+ sig = pyinspect.signature(Message.__init__)
+ is_true("id" in sig.parameters)
+ is_true("content" in sig.parameters)
+ is_true("user_id" in sig.parameters)
+
class RelationshipDefaultFactoryTest(fixtures.TestBase):
def test_list(self, dc_decl_base: Type[MappedAsDataclass]):