From a96601ba155e23899882c28738777ed2e79fb4ae Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Fri, 15 May 2026 14:52:42 +0530 Subject: [PATCH 1/2] FIX: Use covariant Sequence for executemany seq_of_parameters (#572) The 1.6.0 signature seq_of_parameters: Union[List[Sequence[Any]], List[Mapping[str, Any]]] regressed type-checking for callers passing a more precise list type such as list[tuple[str, str, str, str, int, str]]. Because typing.List is invariant, mypy rejects it with: error: Argument 2 to "executemany" of "Cursor" has incompatible type "list[tuple[...]]"; expected "list[Sequence[Any]] | list[Mapping[str, Any]]" [arg-type] note: "list" is invariant -- see ... note: Consider using "Sequence" instead, which is covariant In 1.5.0 the parameter was a single arm (List[Sequence[Any]]) which mypy accepted via the gradual-typing escape hatch on Any; the union introduced in 1.6.0 forces mypy to pick a matching arm and invariance kicks in, breaking previously valid call sites. Switch to the covariant Sequence outer container on both the public Cursor.executemany and the internal _transpose_rowwise_to_columnwise helper: seq_of_parameters: Union[ Sequence[Sequence[Any]], Sequence[Mapping[str, Any]], ] This: - accepts list[tuple[...]], list[list[...]], tuple of tuples, etc.; - matches PEP 249 ("sequence of sequences"); - aligns with pyodbc's typing; - keeps the 1.6.0 dict/pyformat support intact. Runtime behaviour is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- mssql_python/cursor.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index f0b1d6a6..39cc67bb 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2013,7 +2013,10 @@ def columns(self, table=None, catalog=None, schema=None, column=None): # Use the helper method to prepare the result set return self._prepare_metadata_result_set(fallback_description=fallback_description) - def _transpose_rowwise_to_columnwise(self, seq_of_parameters: list) -> tuple[list, int]: + def _transpose_rowwise_to_columnwise( + self, + seq_of_parameters: Union[Sequence[Sequence[Any]], Sequence[Mapping[str, Any]]], + ) -> tuple[list, int]: """ Convert sequence of rows (row-wise) into list of columns (column-wise), for array binding via ODBC. Works with both iterables and generators. @@ -2131,7 +2134,9 @@ def _compute_column_type(self, column): return sample_value, None, None, max_decimal_formatted_len def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-statements - self, operation: str, seq_of_parameters: Union[List[Sequence[Any]], List[Mapping[str, Any]]] + self, + operation: str, + seq_of_parameters: Union[Sequence[Sequence[Any]], Sequence[Mapping[str, Any]]], ) -> None: """ Prepare a database operation and execute it against all parameter sequences. From d41013f163825f5f3a753aff282749215a2b7c08 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Fri, 15 May 2026 15:12:19 +0530 Subject: [PATCH 2/2] FIX: Address Copilot review on PR 586 - sync .pyi stub, narrow helper Two follow-ups from the Copilot PR review on #586: 1. mssql_python/mssql_python.pyi still carried the old invariant signature seq_of_parameters: Union[List[Sequence[Any]], List[Mapping[str, Any]]] Sync it to the covariant Sequence form to match cursor.py and prevent a silent regression if the stub is ever relocated to __init__.pyi (where mypy would actually consume it). 2. _transpose_rowwise_to_columnwise is only ever called with already-converted positional rows (pyformat-to-qmark conversion happens upstream in executemany before transposition). Advertising it as accepting Mapping rows is misleading - the transpose loop would yield mapping keys, not values. Narrow its annotation to Sequence[Sequence[Any]] only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- mssql_python/cursor.py | 2 +- mssql_python/mssql_python.pyi | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 39cc67bb..2341ffa9 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2015,7 +2015,7 @@ def columns(self, table=None, catalog=None, schema=None, column=None): def _transpose_rowwise_to_columnwise( self, - seq_of_parameters: Union[Sequence[Sequence[Any]], Sequence[Mapping[str, Any]]], + seq_of_parameters: Sequence[Sequence[Any]], ) -> tuple[list, int]: """ Convert sequence of rows (row-wise) into list of columns (column-wise), diff --git a/mssql_python/mssql_python.pyi b/mssql_python/mssql_python.pyi index 9b08913d..ad18756e 100644 --- a/mssql_python/mssql_python.pyi +++ b/mssql_python/mssql_python.pyi @@ -193,7 +193,9 @@ class Cursor: reset_cursor: bool = True, ) -> "Cursor": ... def executemany( - self, operation: str, seq_of_parameters: Union[List[Sequence[Any]], List[Mapping[str, Any]]] + self, + operation: str, + seq_of_parameters: Union[Sequence[Sequence[Any]], Sequence[Mapping[str, Any]]], ) -> None: ... def fetchone(self) -> Optional[Row]: ... def fetchmany(self, size: Optional[int] = None) -> List[Row]: ...