]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
forwards-port cpython issue 141560 for getfullargspec
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 3 Feb 2026 13:53:53 +0000 (08:53 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 3 Feb 2026 15:22:11 +0000 (10:22 -0500)
Fixed issue when using ORM mappings with Python 3.14's :pep:`649` feature
that no longer requires "future annotations", where the ORM's introspection
of the ``__init__`` method of mapped classes would fail if non-present
identifiers in annotations were present.  The vendored ``getfullargspec()``
method has been amended to use ``Format.FORWARDREF`` under Python 3.14 to
prevent resolution of names that aren't present.

Fixes: #13104
References: https://github.com/python/cpython/issues/141560
Change-Id: I6af8026a07131d4a1e28cd7fc2e90509194ae957

doc/build/changelog/unreleased_20/13104.rst [new file with mode: 0644]
lib/sqlalchemy/testing/requirements.py
lib/sqlalchemy/util/__init__.py
lib/sqlalchemy/util/compat.py
lib/sqlalchemy/util/langhelpers.py
test/base/test_typing_utils.py
test/orm/declarative/test_tm_future_annotations_sync.py
test/orm/declarative/test_typed_mapping.py

diff --git a/doc/build/changelog/unreleased_20/13104.rst b/doc/build/changelog/unreleased_20/13104.rst
new file mode 100644 (file)
index 0000000..2571485
--- /dev/null
@@ -0,0 +1,11 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 13104
+
+    Fixed issue when using ORM mappings with Python 3.14's :pep:`649` feature
+    that no longer requires "future annotations", where the ORM's introspection
+    of the ``__init__`` method of mapped classes would fail if non-present
+    identifiers in annotations were present.  The vendored ``getfullargspec()``
+    method has been amended to use ``Format.FORWARDREF`` under Python 3.14 to
+    prevent resolution of names that aren't present.
+
index 63ad553abf6bb9c75f22790a8ed4793c883c4de0..2dd4bf6bcfe54b8c418dd491a776d9cb3eedf458 100644 (file)
@@ -1688,6 +1688,11 @@ class SuiteRequirements(Requirements):
             lambda: util.py314, "Python 3.14 or above not supported"
         )
 
+    @property
+    def pep649(self):
+        """pep649 deferred evaluation of annotations without future mode"""
+        return self.python314
+
     @property
     def cpython(self):
         return exclusions.only_if(
index d4dcde409fd73e361a85e46e95e1e9aed2aed5e4..5d55a691d5768752cd879f1fef3e32bd461bd6b5 100644 (file)
@@ -56,6 +56,7 @@ from .compat import dataclass_fields as dataclass_fields
 from .compat import decode_backslashreplace as decode_backslashreplace
 from .compat import dottedgetter as dottedgetter
 from .compat import freethreading as freethreading
+from .compat import get_annotations as get_annotations
 from .compat import has_refcount_gc as has_refcount_gc
 from .compat import inspect_getfullargspec as inspect_getfullargspec
 from .compat import is64bit as is64bit
@@ -105,7 +106,6 @@ from .langhelpers import format_argspec_init as format_argspec_init
 from .langhelpers import format_argspec_plus as format_argspec_plus
 from .langhelpers import generic_fn_descriptor as generic_fn_descriptor
 from .langhelpers import generic_repr as generic_repr
-from .langhelpers import get_annotations as get_annotations
 from .langhelpers import get_callable_argspec as get_callable_argspec
 from .langhelpers import get_cls_kwargs as get_cls_kwargs
 from .langhelpers import get_func_kwargs as get_func_kwargs
index 8f8cc976778658e410dc21e7281d025bf50b6003..ddf042a4742dbf72adb7129fedf083d50c03084b 100644 (file)
@@ -73,6 +73,21 @@ else:
             return iter(self._parts)
 
 
+if py314:
+
+    import annotationlib
+
+    def get_annotations(obj: Any) -> Mapping[str, Any]:
+        return annotationlib.get_annotations(
+            obj, format=annotationlib.Format.FORWARDREF
+        )
+
+else:
+
+    def get_annotations(obj: Any) -> Mapping[str, Any]:
+        return inspect.get_annotations(obj)
+
+
 class FullArgSpec(typing.NamedTuple):
     args: List[str]
     varargs: Optional[str]
@@ -80,7 +95,7 @@ class FullArgSpec(typing.NamedTuple):
     defaults: Optional[Tuple[Any, ...]]
     kwonlyargs: List[str]
     kwonlydefaults: Optional[Dict[str, Any]]
-    annotations: Dict[str, Any]
+    annotations: Mapping[str, Any]
 
 
 def inspect_getfullargspec(func: Callable[..., Any]) -> FullArgSpec:
@@ -117,7 +132,7 @@ def inspect_getfullargspec(func: Callable[..., Any]) -> FullArgSpec:
         func.__defaults__,
         kwonlyargs,
         func.__kwdefaults__,
-        func.__annotations__,
+        get_annotations(func),
     )
 
 
index 8acc352468658942320425c48bf67d839386f102..6b95979bfdb8e64397adb4dc998fe537891a20fd 100644 (file)
@@ -35,7 +35,6 @@ from typing import Generic
 from typing import Iterator
 from typing import List
 from typing import Literal
-from typing import Mapping
 from typing import NoReturn
 from typing import Optional
 from typing import overload
@@ -58,20 +57,6 @@ _F = TypeVar("_F", bound=Callable[..., Any])
 _MA = TypeVar("_MA", bound="HasMemoized.memoized_attribute[Any]")
 _M = TypeVar("_M", bound=ModuleType)
 
-if compat.py314:
-
-    import annotationlib
-
-    def get_annotations(obj: Any) -> Mapping[str, Any]:
-        return annotationlib.get_annotations(
-            obj, format=annotationlib.Format.FORWARDREF
-        )
-
-else:
-
-    def get_annotations(obj: Any) -> Mapping[str, Any]:
-        return inspect.get_annotations(obj)
-
 
 def restore_annotations(
     cls: type, new_annotations: dict[str, Any]
index 58aa15391b45a8f0ecce5144a9b38d6ea08d2e0f..e66431066fb6bff22472a52dee681d0aaa0c2165 100644 (file)
@@ -8,6 +8,7 @@ from typing import cast
 import typing_extensions
 
 from sqlalchemy import Column
+from sqlalchemy import util
 from sqlalchemy.testing import eq_
 from sqlalchemy.testing import fixtures
 from sqlalchemy.testing import is_
@@ -646,3 +647,24 @@ class TestTyping(fixtures.TestBase):
                     types.add(lt)
                     is_(lt in ti, True)
             eq_(len(ti), len(types), k)
+
+    @requires.pep649
+    def test_pep649_getfullargspec(self):
+        """test for #13104"""
+
+        def foo(x: Frobnizzle):  # type: ignore  # noqa: F821
+            pass
+
+        anno = util.get_annotations(foo)
+        eq_(
+            util.inspect_getfullargspec(foo),
+            util.compat.FullArgSpec(
+                args=["x"],
+                varargs=None,
+                varkw=None,
+                defaults=None,
+                kwonlyargs=[],
+                kwonlydefaults=None,
+                annotations=anno,
+            ),
+        )
index 87f785816910c04b56f801e2834f21068c48b989..e01805ce70d346f3f84645451e5a5c3c0526510a 100644 (file)
@@ -4519,6 +4519,31 @@ class AllYourFavoriteHitsTest(fixtures.TestBase, testing.AssertsCompiledSQL):
         else:
             inh_type.fail()
 
+    @requires.pep649
+    def test_pep649_init(self, decl_base):
+
+        class A(decl_base):
+            __tablename__ = "a"
+
+            id: Mapped[int] = mapped_column(
+                BigInteger, Identity(always=True), primary_key=True
+            )
+
+            bs: Mapped[list[B]] = relationship(back_populates="a")
+
+            def __init__(self, bs: list[B], *args, **kwargs):
+                super().__init__(*args, bs=bs, **kwargs)
+
+        class B(decl_base):
+            __tablename__ = "b"
+
+            id: Mapped[int] = mapped_column(
+                BigInteger, Identity(always=True), primary_key=True
+            )
+
+            a_id: Mapped[int] = mapped_column(ForeignKey("a.id"))
+            a: Mapped[A] = relationship(back_populates="bs")
+
 
 class WriteOnlyRelationshipTest(fixtures.TestBase):
     def _assertions(self, A, B, lazy):
index 9f67bf9474dec75380ddd960bea4abcfb72aa42a..d5d5298a9f80c4abddb33f9c28b61d9e4d077fbf 100644 (file)
@@ -4510,6 +4510,31 @@ class AllYourFavoriteHitsTest(fixtures.TestBase, testing.AssertsCompiledSQL):
         else:
             inh_type.fail()
 
+    @requires.pep649
+    def test_pep649_init(self, decl_base):
+
+        class A(decl_base):
+            __tablename__ = "a"
+
+            id: Mapped[int] = mapped_column(
+                BigInteger, Identity(always=True), primary_key=True
+            )
+
+            bs: Mapped[list[B]] = relationship(back_populates="a")
+
+            def __init__(self, bs: list[B], *args, **kwargs):
+                super().__init__(*args, bs=bs, **kwargs)
+
+        class B(decl_base):
+            __tablename__ = "b"
+
+            id: Mapped[int] = mapped_column(
+                BigInteger, Identity(always=True), primary_key=True
+            )
+
+            a_id: Mapped[int] = mapped_column(ForeignKey("a.id"))
+            a: Mapped[A] = relationship(back_populates="bs")
+
 
 class WriteOnlyRelationshipTest(fixtures.TestBase):
     def _assertions(self, A, B, lazy):