]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
add Python 3.15 support
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 3 Aug 2026 17:58:31 +0000 (13:58 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 4 Aug 2026 04:30:34 +0000 (00:30 -0400)
Backport to 2.0 of the 3.15 support added for 2.1, as the issue is
milestoned to 2.0.x.

Add 3.15 to the noxfile Python matrix, and add both the 3.14 and the
3.15 trove classifiers; 3.14 has been in the noxfile matrix here for
some time but was never added to the classifier list.

Repair the auto-generated docstring for MappedAsDataclass subclasses
under 3.15.  Python 3.15 changed dataclasses so that the class
docstring is no longer rendered when the dataclass is created, and is
instead produced by a descriptor the first time __doc__ is read.  The
dataclass setup swaps a constructed __annotations__ onto the class for
the duration of the dataclass call and restores the original in a
finally block, so by the time that deferred render runs the
annotations are gone and the docstring omits them entirely.  Read
__doc__ while the swapped annotations are still in place, so that the
same docstring is produced on every supported Python.

Also port the docstring assertion from the 2.1 test suite; there was
no equivalent coverage here, so this defect would otherwise go
unnoticed on this branch.  As Python 3.8 and earlier still define
typing.Generic.__new__, and declarative classes are Generic subclasses,
the inspect.signature() call dataclasses uses to render this docstring
reports that __new__ rather than the generated __init__ on those
versions; assert the version-appropriate form in each case.

py315: yes

Fixes: #13477
Change-Id: I452a8bb1392a4417194594bd2e1e37b9e56fee26

doc/build/changelog/unreleased_20/13477.rst [new file with mode: 0644]
lib/sqlalchemy/orm/decl_base.py
noxfile.py
setup.cfg
test/orm/declarative/test_dc_transforms.py
test/orm/declarative/test_dc_transforms_future_anno_sync.py

diff --git a/doc/build/changelog/unreleased_20/13477.rst b/doc/build/changelog/unreleased_20/13477.rst
new file mode 100644 (file)
index 0000000..3fbcfc4
--- /dev/null
@@ -0,0 +1,6 @@
+.. change::
+    :tags: bug, platform
+    :tickets: 13477
+
+    Python 3.15 support has been added and tested, including minimal changes
+    for full compatibility.
index d4f35414cf1760c65ebb027bd8ce83898a44b966..ced1c2f3de6ec767dedc407db6e0797a008a24b9 100644 (file)
@@ -1240,6 +1240,20 @@ class _ClassScanMapperConfig(_MapperConfig):
                 "documentation for additional information.",
                 code="dcte",
             ) from ex
+        else:
+            # as of Python 3.15, dataclasses no longer renders the
+            # auto-generated class docstring immediately; it instead installs
+            # a descriptor that renders the ``__init__`` signature the first
+            # time ``__doc__`` is accessed (see
+            # ``dataclasses._AutoDocstring``).  As the ``finally:`` block
+            # below puts the class' original annotations back, that deferred
+            # render would no longer see the dataclass-oriented annotations
+            # applied above, and would omit them from the docstring entirely.
+            # Read the attribute now, while those annotations are still in
+            # place, so the docstring we generate is the same on every Python
+            # version.
+            klass.__doc__
+
         finally:
             # restore original annotations outside of the dataclasses
             # process; for mixins and __abstract__ superclasses, SQLAlchemy
index 8dba5596a96f8f5690593c565ff18c618b0f6462..29d6295809ab4c1b4a099f269dfc0c6bdfe5b082 100644 (file)
@@ -33,6 +33,7 @@ PYTHON_VERSIONS = [
     "3.13",
     "3.14",
     "3.14t",
+    "3.15",
 ]
 DATABASES = ["sqlite", "sqlite_file", "postgresql", "mysql", "oracle", "mssql"]
 CEXT = ["_auto", "cext", "nocext"]
index 8604d7e0df90d6536309c303b3492d409168c44e..61d82ea5ca6202f97ec4d4c124cf79ae1fa8efac 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -22,6 +22,8 @@ classifiers =
     Programming Language :: Python :: 3.11
     Programming Language :: Python :: 3.12
     Programming Language :: Python :: 3.13
+    Programming Language :: Python :: 3.14
+    Programming Language :: Python :: 3.15
     Programming Language :: Python :: Implementation :: CPython
     Programming Language :: Python :: Implementation :: PyPy
     Topic :: Database :: Front-Ends
index ed29992058f411cc19ca879fb40cb9cf25f13418..aaf50267c4975a3b1bada2283ff7543ff9f52d6f 100644 (file)
@@ -149,6 +149,33 @@ class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase):
             ),
         )
 
+        # the docstring dataclasses generates for us should carry the
+        # annotation for every field.  the exact rendering of each annotation
+        # varies by Python version and by whether or not future annotations
+        # mode is in use, so match only on their presence.  as of Python 3.15
+        # this docstring is rendered lazily upon first access rather than when
+        # the dataclass is created, which is the case #13477 addresses.
+        #
+        # this assertion is present in the 2.1 series in this same test, as
+        # DCTransformsTest.test_basic_constructor_repr_base_cls; it is
+        # written more specifically there, as that series supports Python
+        # 3.10 and above only and renders the annotations differently due to
+        # #12168 and #13021.  it's included here in a version-agnostic form
+        # so that the fix for #13477 has coverage on this branch as well
+        if compat.py39:
+            eq_regex(
+                A.__doc__, r"A\(data: .+, x: .+ = None, bs: .+ = <factory>\)"
+            )
+            eq_regex(B.__doc__, r"B\(data: .+, x: .+ = None\)")
+        else:
+            # on Python 3.8 and earlier, ``typing.Generic`` still defines
+            # ``__new__``; as declarative classes are ``Generic`` subclasses,
+            # the ``inspect.signature()`` call dataclasses uses to render
+            # this docstring reports that ``__new__`` rather than the
+            # generated ``__init__``, so there are no annotations to test
+            eq_(A.__doc__, "A(*args, **kwds)")
+            eq_(B.__doc__, "B(*args, **kwds)")
+
         a2 = A("10", x=5, bs=[B("data1"), B("data2", x=12)])
         eq_(
             repr(a2),
index 52a66430cbb1706598c6293bc9d4322f10ec31a6..6de81650d1f9b4f065d79904bae5ce2f59980ff4 100644 (file)
@@ -158,6 +158,33 @@ class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase):
             ),
         )
 
+        # the docstring dataclasses generates for us should carry the
+        # annotation for every field.  the exact rendering of each annotation
+        # varies by Python version and by whether or not future annotations
+        # mode is in use, so match only on their presence.  as of Python 3.15
+        # this docstring is rendered lazily upon first access rather than when
+        # the dataclass is created, which is the case #13477 addresses.
+        #
+        # this assertion is present in the 2.1 series in this same test, as
+        # DCTransformsTest.test_basic_constructor_repr_base_cls; it is
+        # written more specifically there, as that series supports Python
+        # 3.10 and above only and renders the annotations differently due to
+        # #12168 and #13021.  it's included here in a version-agnostic form
+        # so that the fix for #13477 has coverage on this branch as well
+        if compat.py39:
+            eq_regex(
+                A.__doc__, r"A\(data: .+, x: .+ = None, bs: .+ = <factory>\)"
+            )
+            eq_regex(B.__doc__, r"B\(data: .+, x: .+ = None\)")
+        else:
+            # on Python 3.8 and earlier, ``typing.Generic`` still defines
+            # ``__new__``; as declarative classes are ``Generic`` subclasses,
+            # the ``inspect.signature()`` call dataclasses uses to render
+            # this docstring reports that ``__new__`` rather than the
+            # generated ``__init__``, so there are no annotations to test
+            eq_(A.__doc__, "A(*args, **kwds)")
+            eq_(B.__doc__, "B(*args, **kwds)")
+
         a2 = A("10", x=5, bs=[B("data1"), B("data2", x=12)])
         eq_(
             repr(a2),