Skip to content

Commit 09f7fb3

Browse files
Reject unknown cursor keyword arguments for PyAthena 4.0
1 parent 23a54e1 commit 09f7fb3

28 files changed

Lines changed: 553 additions & 25 deletions

‎docs/cursor.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# Cursor
22

3+
## Keyword arguments in 4.0.0
4+
5+
Starting in 4.0.0, cursor constructors and `execute()` raise `TypeError` for unknown keyword arguments.
6+
Earlier versions silently ignored some extra arguments.
7+
Correct misspelled names and pass backend options to the cursor that supports them:
8+
9+
```python
10+
cursor = connection.cursor(work_group="analytics")
11+
cursor.execute("SELECT 1", work_group="analytics")
12+
```
13+
14+
The validation happens before starting a query or a Spark calculation.
15+
For native asyncio cursors, `execute()` validates arguments when awaited.
16+
Pandas and Polars cursors continue to accept their reader options, with names checked against the installed library and the selected CSV, UNLOAD, and chunking mode.
17+
Invalid values and combinations remain subject to the reader's validation.
18+
Arrow and S3FS cursors accept their supported result-set options; arbitrary reader options are rejected.
19+
20+
Connection-level `on_start_query_execution` and `on_poll` remain accepted by every cursor constructor.
21+
The Future-based Async cursors and Spark cursors do not invoke `on_start_query_execution`.
22+
For reader options, such as pandas `parse_dates`, pass the option to `execute()` rather than the constructor.
23+
324
(default_cursor)=
425

526
## DefaultCursor

‎pyathena/_kwargs.py‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright 2026 The PyAthena authors
2+
#
3+
# Licensed under the MIT License.
4+
# See LICENSE or https://opensource.org/licenses/MIT.
5+
#
6+
# SPDX-License-Identifier: MIT
7+
8+
"""Validation of keyword names forwarded by cursors."""
9+
10+
from collections.abc import Callable, Collection
11+
from inspect import Parameter, signature
12+
from typing import Any
13+
14+
15+
def keyword_parameters(func: Callable[..., Any]) -> set[str]:
16+
"""Return the explicitly named parameters that accept keyword arguments.
17+
18+
Args:
19+
func: The callable whose signature to inspect.
20+
21+
Returns:
22+
Its positional-or-keyword and keyword-only parameter names.
23+
"""
24+
return {
25+
name
26+
for name, parameter in signature(func).parameters.items()
27+
if parameter.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
28+
}
29+
30+
31+
def constructor_keyword_parameters(cls: type[Any]) -> set[str]:
32+
"""Return explicit constructor keyword names across a class's MRO.
33+
34+
Args:
35+
cls: The class whose forwarding constructors to inspect.
36+
37+
Returns:
38+
The named constructor parameters, excluding ``self``.
39+
"""
40+
names: set[str] = set()
41+
for base in cls.__mro__:
42+
if "__init__" in vars(base):
43+
names.update(keyword_parameters(vars(base)["__init__"]))
44+
return names - {"self"}
45+
46+
47+
def validate_kwargs(method: str, kwargs: dict[str, Any], allowed: Collection[str] = ()) -> None:
48+
"""Reject the first keyword name that a cursor does not support.
49+
50+
Args:
51+
method: The method name included in the error.
52+
kwargs: Extra keyword arguments given to the method.
53+
allowed: The extra keyword names the method supports.
54+
55+
Raises:
56+
TypeError: If a keyword name is not allowed.
57+
"""
58+
for name in kwargs:
59+
if name not in allowed:
60+
raise TypeError(f"{method}() got an unexpected keyword argument '{name}'")

‎pyathena/aio/arrow/cursor.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from collections.abc import Callable
88
from typing import TYPE_CHECKING, Any, cast
99

10+
from pyathena._kwargs import validate_kwargs
1011
from pyathena.aio.common import WithAsyncFetch
1112
from pyathena.arrow.converter import (
1213
DefaultArrowTypeConverter,
@@ -143,13 +144,19 @@ async def execute(
143144
options: Shared execution options as an
144145
:class:`~pyathena.options.ExecuteOptions` instance. Individual
145146
keyword arguments take precedence over ``options`` fields.
146-
**kwargs: Additional execution parameters.
147+
**kwargs: Result-set overrides: ``block_size``, ``connect_timeout``,
148+
and ``request_timeout``. Unknown names raise TypeError.
147149
``block_size`` sets the read block size for this query, and
148150
``connect_timeout`` and ``request_timeout`` override the cursor's values.
149151
150152
Returns:
151153
Self reference for method chaining.
152154
"""
155+
validate_kwargs(
156+
f"{type(self).__name__}.execute",
157+
kwargs,
158+
("block_size", "connect_timeout", "request_timeout"),
159+
)
153160
self._reset_state()
154161
options = ExecuteOptions.resolve(
155162
options,

‎pyathena/aio/cursor.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from collections.abc import Callable
1414
from typing import Any, cast
1515

16+
from pyathena._kwargs import validate_kwargs
1617
from pyathena.aio.common import WithAsyncFetch
1718
from pyathena.aio.result_set import AthenaAioDictResultSet, AthenaAioResultSet
1819
from pyathena.common import CursorIterator
@@ -138,11 +139,12 @@ async def execute(
138139
options: Shared execution options as an
139140
:class:`~pyathena.options.ExecuteOptions` instance. Individual
140141
keyword arguments take precedence over ``options`` fields.
141-
**kwargs: Additional execution parameters.
142+
**kwargs: Unknown keyword arguments raise TypeError.
142143
143144
Returns:
144145
Self reference for method chaining.
145146
"""
147+
validate_kwargs(f"{type(self).__name__}.execute", kwargs)
146148
self._reset_state()
147149
options = ExecuteOptions.resolve(
148150
options,

‎pyathena/aio/pandas/cursor.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
DefaultPandasTypeConverter,
2222
DefaultPandasUnloadTypeConverter,
2323
)
24-
from pyathena.pandas.result_set import AthenaPandasResultSet, PandasDataFrameIterator
24+
from pyathena.pandas.result_set import (
25+
AthenaPandasResultSet,
26+
PandasDataFrameIterator,
27+
validate_execute_kwargs,
28+
)
2529
from pyathena.util import override
2630

2731
if TYPE_CHECKING:
@@ -176,6 +180,7 @@ async def execute(
176180
Returns:
177181
Self reference for method chaining.
178182
"""
183+
validate_execute_kwargs(f"{type(self).__name__}.execute", kwargs, self._unload)
179184
self._reset_state()
180185
options = ExecuteOptions.resolve(
181186
options,

‎pyathena/aio/polars/cursor.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
DefaultPolarsTypeConverter,
1818
DefaultPolarsUnloadTypeConverter,
1919
)
20-
from pyathena.polars.result_set import AthenaPolarsResultSet
20+
from pyathena.polars.result_set import AthenaPolarsResultSet, validate_execute_kwargs
2121
from pyathena.util import override
2222

2323
if TYPE_CHECKING:
@@ -160,6 +160,12 @@ async def execute(
160160
Returns:
161161
Self reference for method chaining.
162162
"""
163+
validate_execute_kwargs(
164+
f"{type(self).__name__}.execute",
165+
kwargs,
166+
self._unload,
167+
kwargs.get("chunksize", self._chunksize),
168+
)
163169
self._reset_state()
164170
options = ExecuteOptions.resolve(
165171
options,

‎pyathena/aio/s3fs/cursor.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from collections.abc import Callable
1515
from typing import Any
1616

17+
from pyathena._kwargs import validate_kwargs
1718
from pyathena.aio.common import WithAsyncFetch
1819
from pyathena.common import CursorIterator
1920
from pyathena.error import OperationalError
@@ -146,13 +147,14 @@ async def execute(
146147
options: Shared execution options as an
147148
:class:`~pyathena.options.ExecuteOptions` instance. Individual
148149
keyword arguments take precedence over ``options`` fields.
149-
**kwargs: Additional execution parameters.
150+
**kwargs: Supported S3FS result-set overrides. Unknown names raise TypeError.
150151
``block_size`` sets the read block size for this query, and
151152
``csv_reader`` overrides the cursor's value.
152153
153154
Returns:
154155
Self reference for method chaining.
155156
"""
157+
validate_kwargs(f"{type(self).__name__}.execute", kwargs, ("block_size", "csv_reader"))
156158
self._reset_state()
157159
options = ExecuteOptions.resolve(
158160
options,

‎pyathena/aio/spark/cursor.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import uuid
1515
from typing import Any, cast
1616

17+
from pyathena._kwargs import validate_kwargs
1718
from pyathena.aio.util import async_retry_api_call
1819
from pyathena.error import DatabaseError, NotSupportedError, OperationalError, ProgrammingError
1920
from pyathena.model import (
@@ -352,11 +353,12 @@ async def execute(
352353
description: Calculation description.
353354
client_request_token: Idempotency token.
354355
work_group: Unused, kept for API compatibility.
355-
**kwargs: Additional parameters.
356+
**kwargs: Unknown keyword arguments raise TypeError.
356357
357358
Returns:
358359
Self reference for method chaining.
359360
"""
361+
validate_kwargs(f"{type(self).__name__}.execute", kwargs)
360362
# A failure below must not leave the previous calculation on the cursor.
361363
self._calculation_id = None
362364
self._calculation_execution = None

‎pyathena/aio/sqlalchemy/base.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,11 @@ def cursor(self, cursor: Any = None, **kwargs: Any) -> AsyncAdapt_pyathena_curso
276276
raw_cursor = self._connection.cursor(_ASYNC_CURSOR_CLASSES.get(cursor, cursor), **kwargs)
277277
return AsyncAdapt_pyathena_cursor(raw_cursor)
278278

279+
def _internal_cursor(self, cursor: Any) -> AsyncAdapt_pyathena_cursor:
280+
"""Adapt an API cursor with the default backend's result settings excluded."""
281+
raw_cursor = self._connection._internal_cursor(_ASYNC_CURSOR_CLASSES.get(cursor, cursor))
282+
return AsyncAdapt_pyathena_cursor(raw_cursor)
283+
279284
def close(self) -> None:
280285
"""Close the wrapped connection."""
281286
self._connection.close()

‎pyathena/arrow/async_cursor.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Any, cast
99

1010
from pyathena import ProgrammingError
11+
from pyathena._kwargs import validate_kwargs
1112
from pyathena.arrow.converter import (
1213
DefaultArrowTypeConverter,
1314
DefaultArrowUnloadTypeConverter,
@@ -211,13 +212,19 @@ def execute(
211212
options: Shared execution options as an
212213
:class:`~pyathena.options.ExecuteOptions` instance. Individual
213214
keyword arguments take precedence over ``options`` fields.
214-
**kwargs: Additional execution parameters.
215+
**kwargs: Result-set overrides: ``block_size``, ``connect_timeout``,
216+
and ``request_timeout``. Unknown names raise TypeError.
215217
``block_size`` sets the read block size for this query, and
216218
``connect_timeout`` and ``request_timeout`` override the cursor's values.
217219
218220
Returns:
219221
Tuple of (query_id, future) where future resolves to AthenaArrowResultSet.
220222
"""
223+
validate_kwargs(
224+
f"{type(self).__name__}.execute",
225+
kwargs,
226+
("block_size", "connect_timeout", "request_timeout"),
227+
)
221228
options = ExecuteOptions.resolve(
222229
options,
223230
work_group=work_group,

0 commit comments

Comments
 (0)