From: proto-atlas <278522736+proto-atlas@users.noreply.github.com> Date: Mon, 25 May 2026 19:14:40 +0000 (-0400) Subject: Fix Session bulk mappings typing for mapped classes X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=8f882575b9bbcbca891360b940d671aac6c3c051;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Fix Session bulk mappings typing for mapped classes Fixes #9256. This updates the annotations for Session.bulk_insert_mappings() and Session.bulk_update_mappings(). The docstrings and runtime behavior already allow either a mapped class or a Mapper object, but the previous annotations only accepted Mapper[Any]. This patch switches those arguments to the existing _EntityBindKey alias, which matches the inputs accepted by _class_to_mapper(): mapped classes and Mapper objects, but not AliasedClass or AliasedInsp. I also updated the internal _bulk_save_mappings() annotation so the public methods and the private helper stay consistent. The scoped_session proxy output has been kept in sync with tools/generate_proxy_methods.py, and the generator check passes. I added a typing regression test covering both mapped classes and Mapper objects for the two bulk mapping methods. I confirmed that the mapped-class cases fail with the old annotation and pass with this change. Checked locally: python -m pytest -m mypy test/typing/test_mypy.py -k "session.py" -q python -m mypy ./lib/sqlalchemy python tools/generate_proxy_methods.py --check python -m pytest test/orm/dml/test_bulk.py -q python -m pytest -m mypy test/typing/test_mypy.py -k "not typed_queries.py" -q I could not run the full typing suite locally because my local Python 3.12 environment does not include string.templatelib. I only skipped typed_queries.py; that file is expected to be covered by SQLAlchemy's Python 3.14 mypy CI job. Closes: #13322 Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/13322 Pull-request-sha: bb730f34275a7c40c94e668ecda1131804ba3084 Change-Id: I4c5d516b3933b4e7fae9c844881a61f557a8bb5e (cherry picked from commit e00937ec549ecc1d2cae49d4fe84fac3d758b6fb) --- diff --git a/lib/sqlalchemy/orm/scoping.py b/lib/sqlalchemy/orm/scoping.py index b8ae381a44..e6d3f84f10 100644 --- a/lib/sqlalchemy/orm/scoping.py +++ b/lib/sqlalchemy/orm/scoping.py @@ -39,7 +39,6 @@ if TYPE_CHECKING: from ._typing import OrmExecuteOptionsParameter from .identity import IdentityMap from .interfaces import ORMOption - from .mapper import Mapper from .query import Query from .query import RowReturningQuery from .session import _BindArguments @@ -1350,7 +1349,7 @@ class scoped_session(Generic[_S]): def bulk_insert_mappings( self, - mapper: Mapper[Any], + mapper: _EntityBindKey[Any], mappings: Iterable[Dict[str, Any]], return_defaults: bool = False, render_nulls: bool = False, @@ -1436,7 +1435,7 @@ class scoped_session(Generic[_S]): ) def bulk_update_mappings( - self, mapper: Mapper[Any], mappings: Iterable[Dict[str, Any]] + self, mapper: _EntityBindKey[Any], mappings: Iterable[Dict[str, Any]] ) -> None: r"""Perform a bulk update of the given list of mapping dictionaries. diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index a10d943723..4b5cf0dd26 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -4594,7 +4594,7 @@ class Session(_SessionClassMethods, EventTarget): def bulk_insert_mappings( self, - mapper: Mapper[Any], + mapper: _EntityBindKey[Any], mappings: Iterable[Dict[str, Any]], return_defaults: bool = False, render_nulls: bool = False, @@ -4676,7 +4676,7 @@ class Session(_SessionClassMethods, EventTarget): ) def bulk_update_mappings( - self, mapper: Mapper[Any], mappings: Iterable[Dict[str, Any]] + self, mapper: _EntityBindKey[Any], mappings: Iterable[Dict[str, Any]] ) -> None: """Perform a bulk update of the given list of mapping dictionaries. @@ -4725,7 +4725,7 @@ class Session(_SessionClassMethods, EventTarget): def _bulk_save_mappings( self, - mapper: Mapper[_O], + mapper: _EntityBindKey[_O], mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]], *, isupdate: bool, diff --git a/test/typing/plain_files/orm/session.py b/test/typing/plain_files/orm/session.py index 5513dd7e5d..f61527c18b 100644 --- a/test/typing/plain_files/orm/session.py +++ b/test/typing/plain_files/orm/session.py @@ -5,6 +5,7 @@ from typing import List from sqlalchemy import create_engine from sqlalchemy import ForeignKey +from sqlalchemy import inspect from sqlalchemy.ext.asyncio import async_scoped_session from sqlalchemy.ext.asyncio import async_sessionmaker from sqlalchemy.ext.asyncio import AsyncSession @@ -103,6 +104,12 @@ with Session(e) as sess: # EXPECTED_TYPE: SessionTransaction reveal_type(tx) + # test #9256 + sess.bulk_insert_mappings(User, [{"id": 1, "name": "u1"}]) + sess.bulk_update_mappings(User, [{"id": 1, "name": "u1"}]) + sess.bulk_insert_mappings(inspect(User), [{"id": 2, "name": "u2"}]) + sess.bulk_update_mappings(inspect(User), [{"id": 2, "name": "u2"}]) + # more result tests in typed_results.py