Repository navigation
BREAKING: Reject unknown cursor keyword arguments #1102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
09f7fb3
0241fc2
fbb2832
7496dcd
6d6fe2f
f2460c5
5b48a62
5c96c92
769b115
3daaedf
bb993fa
c42d65d
45639cd
a8d79da
2bc1ef0
dd296bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,9 +7,10 @@ | |
| import threading | ||
| import time | ||
| from abc import ABCMeta, abstractmethod | ||
| from collections.abc import Callable | ||
| from collections.abc import Callable, Collection | ||
| from concurrent.futures import Future, wait | ||
| from datetime import UTC, datetime, timedelta | ||
| from inspect import Parameter, signature | ||
| from typing import TYPE_CHECKING, Any, TypeVar, cast | ||
|
|
||
| from botocore.exceptions import BotoCoreError, ClientError | ||
|
|
@@ -212,7 +213,6 @@ def __init__( | |
| result_reuse_minutes: int, | ||
| on_start_query_execution: Callable[[str], None] | None = None, | ||
| on_poll: OnPollCallback | None = None, | ||
| **kwargs, | ||
| ) -> None: | ||
| """Initialize the cursor with the settings it uses to run queries. | ||
|
|
||
|
|
@@ -236,7 +236,6 @@ def __init__( | |
| waits for the query, by cursors whose ``execute()`` supports it. | ||
| on_poll: Callback invoked once per poll iteration with the current | ||
| execution object. | ||
| **kwargs: Ignored. | ||
| """ | ||
| super().__init__() | ||
| self._connection = connection | ||
|
|
@@ -272,11 +271,48 @@ def get_default_converter(unload: bool = False) -> DefaultTypeConverter | Any: | |
| """ | ||
| return DefaultTypeConverter() | ||
|
|
||
| @classmethod | ||
| def _constructor_keyword_names(cls) -> set[str]: | ||
| """Get the keyword names accepted by the constructors in this class's MRO. | ||
|
|
||
| Returns: | ||
| The named parameters of each ``__init__`` defined in the MRO, | ||
| excluding ``self``. | ||
| """ | ||
| names: set[str] = set() | ||
| for base in cls.__mro__: | ||
| if "__init__" in vars(base): | ||
| names.update( | ||
| name | ||
| for name, parameter in signature(vars(base)["__init__"]).parameters.items() | ||
| if parameter.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY) | ||
| ) | ||
| names.discard("self") | ||
| return names | ||
|
|
||
| @property | ||
| def connection(self) -> Connection[Any]: | ||
| """The connection that created this cursor.""" | ||
| return self._connection | ||
|
|
||
| def _validate_execute_kwargs( | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Self-review round one (implementation behavior) — FINDINGS, repaired. Base: Scope: the reworked PR after removing pandas/Polars reader pre-validation. Covered every Repaired in
Deferred with reasons:
Validation at
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Round-one repair follow-up at
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Round-one repair follow-up at |
||
| self, kwargs: dict[str, Any], allowed: Collection[str] = () | ||
| ) -> None: | ||
| """Reject the keyword arguments that ``execute()`` does not support. | ||
|
|
||
| Args: | ||
| kwargs: The extra keyword arguments given to ``execute()``. | ||
| allowed: The extra keyword names that this cursor supports. | ||
|
|
||
| Raises: | ||
| TypeError: If a keyword name is not in ``allowed``. | ||
| """ | ||
| for name in kwargs: | ||
| if name not in allowed: | ||
| raise TypeError( | ||
| f"{type(self).__name__}.execute() got an unexpected keyword argument '{name}'" | ||
| ) | ||
|
|
||
| def _build_start_query_execution_request( | ||
| self, | ||
| query: str, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -612,7 +612,21 @@ def cursor( | |
| >>> pandas_cursor = connection.cursor(PandasCursor) | ||
| >>> df = pandas_cursor.execute("SELECT * FROM my_table").fetchall() | ||
| """ | ||
| kwargs = {**self.cursor_kwargs, **kwargs} | ||
| return self._cursor(cursor, **{**self.cursor_kwargs, **kwargs}) | ||
|
|
||
| def _cursor( | ||
| self, cursor: type[FunctionalCursor] | None, /, **kwargs: Any | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Relayed independent review — Codex CLI (codex-companion Initial review: base FINDINGS:
Follow-up: bounded review of Author validation at
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Relayed independent follow-up — Codex CLI (codex-companion |
||
| ) -> FunctionalCursor | ConnectionCursor: | ||
| """Create a cursor without applying the connection's ``cursor_kwargs``. | ||
|
|
||
| Args: | ||
| cursor: The cursor class. If None, the connection's default cursor class. | ||
| **kwargs: The cursor constructor arguments. Omitted connection settings | ||
| default to the connection's values. | ||
|
|
||
| Returns: | ||
| The created cursor. | ||
| """ | ||
| _cursor = cursor or self.cursor_class | ||
| converter = kwargs.pop("converter", self._converter) | ||
| if not converter: | ||
|
|
@@ -639,6 +653,26 @@ def cursor( | |
| **kwargs, | ||
| ) | ||
|
|
||
| def _internal_cursor(self, cursor: type[FunctionalCursor]) -> FunctionalCursor: | ||
| """Create a cursor for queries that PyAthena runs on its own behalf. | ||
|
|
||
| Only the connection's ``cursor_kwargs`` that the cursor class accepts are | ||
| applied, leaving out options of the default cursor class such as ``unload`` | ||
| of a DataFrame cursor. ``arraysize`` is left out too, since a DataFrame | ||
| cursor allows values above this cursor's page size limit. The cursor | ||
| class's default converter is used. | ||
|
|
||
| Args: | ||
| cursor: The cursor class. | ||
|
|
||
| Returns: | ||
| The created cursor. | ||
| """ | ||
| accepted = cursor._constructor_keyword_names() - {"arraysize"} | ||
| kwargs = {k: v for k, v in self.cursor_kwargs.items() if k in accepted} | ||
| kwargs["converter"] = cursor.get_default_converter() | ||
| return cast(FunctionalCursor, self._cursor(cursor, **kwargs)) | ||
|
|
||
| def close(self) -> None: | ||
| """Close the connection. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -536,9 +536,7 @@ def _internal_cursor(raw_connection: PoolProxiedConnection) -> Any: | |
| returns its wrapper, so this is typed by the interface used here rather | ||
| than by the class requested. | ||
| """ | ||
| return raw_connection.driver_connection.cursor( # type: ignore[union-attr] | ||
| Cursor, converter=Cursor.get_default_converter() | ||
| ) | ||
| return raw_connection.driver_connection._internal_cursor(Cursor) # type: ignore[union-attr] | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Relayed independent static review — Claude Code, claude-opus-5-5, Max profile, effort high, first-party authentication (not Enterprise). Session: 42543ff2-b082-4c60-b3f9-176757cbc8fc. FINDINGS — Medium: Existing dialect test doubles still provide only cursor(), so 13 information_schema and view-definition unit cases raise AttributeError after this call changes to _internal_cursor(). Affected factories are in tests/pyathena/sqlalchemy/test_base.py:185,353,390,443,483,576,607. Their old converter-call assertions also need updating. Verified by the local AWS-suite run. The production Connection and async adapter implement this method. A further integration baseline at test_base.py:1417 passes backend unload defaults to public Cursor and must instead create an isolated baseline connection.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Author repair record. Current reviewed head: f2460c5 Updated every affected dialect test factory to implement _internal_cursor(), preserving the existing reflection/error/cache assertions. The view-definition runtime baseline now uses a separate API Connection. Repaired in 0241fc2; all affected unit cases pass. Both self-review perspectives and the applicable independent follow-ups are complete. The last code follow-up found no regressions; the final wording follow-up is CLEAN. Local focused offline tests: 486 passed on 6d6fe2f (only the two-line docstring clarification follows). Current-head lint/docs/offline CI has passed. AWS CI is still pending and will run after Ready. |
||
|
|
||
| def _column(self, name: str | None, type_: str, comment: str | None, partition: bool | None): | ||
| return { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Self-review round two (claims, callers, operations) — FINDINGS, repaired.
Base:
f3532e9fb57fd769441e533f0b8375fd854ece69Head:
a8d79da40e176d553555233c0e5fa5cbb110569dClaims checked:
pd.read_csv,pd.read_parquet(pyarrowread_table),pl.read_csv,pl.scan_csv,pl.read_parquet, andpl.scan_parquetraise TypeError; the result sets wrap it asOperationalError(pyathena/pandas/result_set.py,pyathena/polars/result_set.pyexcept Exception)._read_csvreturns an empty frame before calling the reader when there is no readable output, so the docs now say so.vars(cursor)and fail on any_make_api_call; Future-based cursors validate before_execute/submit; Spark before_calculate.on_start_query_execution/on_pollstill accepted by every cursor:test_cursor_accepts_connection_callbacks(sync and aio).connections_legacy.py(dbt-labs/dbt-adapters main) buildsAthenaCursor(**kwargs)→Cursor.__init__(**kwargs)withcursor_kwargs={"debug_query_state", "num_iceberg_retries"}and reads them back fromconnection.cursor_kwargs. That now raises TypeError. It pinspyathena>=2.25,<3.35, and 4.0.0 already requires it to change (BREAKING: Make connection and cursor arguments keyword-only #1101, Remove the legacy keyword arguments from the private _execute() #1088). Added the subclass rule todocs/usage.md:45and the example to the PR's release-note text.No unmeasured performance claim remains: the reworked PR adds a dict-key loop to non-reader
execute()and no signature inspection on query paths.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Round-two repair follow-up at
2bc1ef0e09ce17fb2a81adbf1dfcccaaa61ec26b: CLEAN. Claim corrected indocs/usage.md:47and the PR body: managed query results (_fetch_all_rows_as_csv) invoke the reader without a result file, so the unchecked case is "no results to read" (no output and empty UNLOAD manifests). No other claim changed.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Round-two repair follow-up at
dd296bf5d231c5025f2f428384eec44a35ae876b: CLEAN. Docstring claim checked: DataFrame/Arrow/S3FS cursors have no arraysize upper bound (WithResultSet), whileCursor/AioCursorcap it atDEFAULT_FETCH_SIZE(1000). The internal cursor only reads reflection rows, so using its default page size changes no user-visible result. PR body updated.