Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,5 @@ Versions are derived from git tags via `hatch-vcs` — never edit `pyathena/_ver

### Google-style Docstrings

Use Google-style docstrings for public methods. See existing code for examples.
Follow the [docstring rules](docs/contributing.md#write-docstrings), which `just lint` checks with ruff's pydocstyle rules for `pyathena/`.
Mark overrides with `pyathena.util.override` instead of repeating the base method's docstring.
20 changes: 20 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@ External-fork pull requests must not be run in the project's AWS integration CI.
Do not submit an untested change expecting a maintainer to approve an AWS CI run to validate it.
Checks that need no AWS access may still run, but their success does not establish integration coverage.

## Write docstrings

Code under `pyathena/` uses [Google-style docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings).
`just lint` checks them with ruff's pydocstyle rules.

- Modules, packages, public classes, and public functions and methods have a docstring.
Describe arguments, return values, and raised exceptions in `Args:`, `Returns:`, and `Raises:` sections.
- `__init__` describes the constructor arguments in its `Args:` section.
- A property getter has a one-line docstring; its setter needs none.
- Magic methods such as `__enter__` and `__iter__` need no docstring.
- A method that overrides a base class method is decorated with `override` from `pyathena.util`.
It needs a docstring only when its behavior differs from the base method; otherwise the API reference shows the base method's docstring.
mypy reports a missing decorator, except on an unannotated property.
- Overrides of fsspec methods are not decorated, because fsspec has no type information, so they need a docstring.
- New or changed private functions and methods use the same style.
ruff does not require them to have a docstring, but checks the docstrings they have.

`per-file-ignores` in `pyproject.toml` lists the `D` codes that each file still has findings for.
Remove a file's `D` codes when its docstrings are complete, and keep its other codes.

## Open a pull request

Open a draft pull request with the repository's template completed.
Expand Down
6 changes: 5 additions & 1 deletion pyathena/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@


class DBAPITypeObject(frozenset[str]):
"""Type Objects and Constructors
"""A DB API type object that compares equal to each of its Athena type names.

https://www.python.org/dev/peps/pep-0249/#type-objects-and-constructors
"""
Expand Down Expand Up @@ -89,6 +89,8 @@ def connect(*args, **kwargs) -> Connection[Any]:
SQL queries.

Args:
*args: Positional arguments passed to the Connection constructor, in the
order of its parameters (``s3_staging_dir``, ``region_name``, ...).
s3_staging_dir: S3 location to store query results. Required if not
using workgroups or if the workgroup doesn't have a result location.
Pass an empty string to explicitly disable S3 staging and skip
Expand Down Expand Up @@ -144,6 +146,8 @@ async def aio_connect(*args, **kwargs) -> AioConnection:
and API calls, keeping the event loop free.

Args:
*args: Forwarded to ``AioConnection.create()``, which accepts keyword
arguments only.
**kwargs: Arguments forwarded to ``AioConnection.create()``.
See :func:`connect` for the full list of supported arguments.

Expand Down
2 changes: 1 addition & 1 deletion pyathena/aio/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ async def execute(
parameters: Query parameters (optional).
work_group: Athena workgroup to use (optional).
s3_staging_dir: S3 location for query results (optional).
cache_size: Query result cache size (optional).
cache_size: Number of queries to check for result caching (optional).
cache_expiration_time: Cache expiration time in seconds (optional).
result_reuse_enable: Enable result reuse (optional).
result_reuse_minutes: Result reuse duration in minutes (optional).
Expand Down
2 changes: 1 addition & 1 deletion pyathena/async_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def execute(
parameters: Query parameters (optional).
work_group: Athena workgroup to use (optional).
s3_staging_dir: S3 location for query results (optional).
cache_size: Query result cache size in MB (optional).
cache_size: Number of queries to check for result caching (optional).
cache_expiration_time: Cache expiration time in seconds (optional).
result_reuse_enable: Enable result reuse for identical queries (optional).
result_reuse_minutes: Result reuse duration in minutes (optional).
Expand Down
14 changes: 12 additions & 2 deletions pyathena/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1232,10 +1232,20 @@ def _cancel(self, query_id: str) -> None:
raise OperationalError(*e.args) from e

def setinputsizes(self, sizes): # noqa: B027
"""Does nothing by default"""
"""Accept input sizes as DB API 2.0 requires, and ignore them.

Args:
sizes: Sequence of parameter types or sizes.
"""

def setoutputsize(self, size, column=None): # noqa: B027
"""Does nothing by default"""
"""Accept a column buffer size as DB API 2.0 requires, and ignore it.

Args:
size: Buffer size for large columns.
column: Index of the column the size applies to, or None for all
large columns.
"""

def __enter__(self):
return self
Expand Down
7 changes: 7 additions & 0 deletions pyathena/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ def execute(
Args:
operation: SQL query string to execute.
parameters: Query parameters (optional).
work_group: Athena workgroup to use for this query.
s3_staging_dir: S3 location for query results.
cache_size: Number of queries to check for result caching.
cache_expiration_time: Cache expiration time in seconds.
result_reuse_enable: Enable Athena result reuse for this query.
result_reuse_minutes: Minutes to reuse cached results.
paramstyle: Parameter style ('qmark' or 'pyformat').
on_start_query_execution: Callback function called immediately after
start_query_execution API is called.
Function signature: (query_id: str) -> None
Expand Down
5 changes: 5 additions & 0 deletions pyathena/filesystem/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -1964,6 +1964,11 @@ def _call(self, method: str | Callable[..., Any], **kwargs) -> dict[str, Any]:


class S3File(AbstractBufferedFile):
"""A buffered file object for reading and writing an S3 object.

Instances are returned by ``S3FileSystem.open()``.
"""

fs: S3FileSystem

def __init__(
Expand Down
2 changes: 2 additions & 0 deletions pyathena/result_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,8 @@ def __exit__(self, exc_type, exc_val, exc_tb):


class AthenaDictResultSet(AthenaResultSet):
"""A result set that returns each row as a dictionary keyed by column name."""

# You can override this to use OrderedDict or other dict-like types.
dict_type: type[Any] = dict

Expand Down
2 changes: 2 additions & 0 deletions pyathena/sqlalchemy/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@


class Requirements(SuiteRequirements):
"""Features of the Athena dialect for the SQLAlchemy test suite."""

@property
@override
def comment_reflection(self):
Expand Down
100 changes: 100 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -159,22 +159,122 @@ select = [
"PGH", # pygrep-hooks
"G", # flake8-logging-format
"PT", # flake8-pytest-style
"D", # pydocstyle
]
ignore = [
"RUF059", # unused-unpacked-variable (too noisy for interface-heavy code)
"G004", # logging-f-string (f-strings are preferred for log messages)
"D105", # undocumented-magic-method (protocol methods need no docstring)
]

[tool.ruff.lint.pydocstyle]
convention = "google"
# Overrides marked with @override inherit the base method's documentation.
ignore-decorators = ["pyathena.util.override"]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review round 1 (implementation behavior): CLEAN

Base 9ef49d311a7124ef90a58703d02b14e612a17bdc, head 06ad69063963497fd2d6423ad9cfca296b18c067. All 11 changed files covered.

Covered:

  • Rule scope: !pyathena/** disables D outside the package, so tests/, scripts/ and docs/conf.py are not checked. Inside the package, a new file and an undocumented class are reported (mutations reverted). ignore-decorators exempts @override methods. D105 is ignored. The per-file entries match the measured findings (83 files, 508 findings) and do not hide the D101/D415/D417 codes.
  • Docstring facts checked against the code:
    • connect(*args) follows Connection.__init__'s parameter order (s3_staging_dir, region_name, ...) in both overloads and the implementation.
    • aio_connect(*args) raises TypeError because AioConnection.create(cls, **kwargs) takes keyword arguments only.
    • S3FileSystem._open returns S3File.
    • AthenaDictResultSet._get_rows builds rows with dict_type from the column names.
    • DBAPITypeObject.__eq__ uses other in self.
    • cache_size is the number of recent executions searched (common.py _find_previous_query_id), not a size in MB.
    • setinputsizes/setoutputsize are empty in BaseCursor. AsyncAdapt_pyathena_cursor.setinputsizes in the async dialect is a separate adapter method and is unaffected.
  • Runtime: only docstrings and configuration change. No code path changes.
  • Tests: none needed. The guard is just lint, and the rule scope was checked by mutation.

Findings: none.


[tool.ruff.lint.per-file-ignores]
"tests/**" = [
"RUF012", # mutable-class-default (test classes often use mutable defaults)
]
"!pyathena/**" = [
"D", # docstring rules apply to the package only
]
"pyathena/sqlalchemy/compiler.py" = [
"N802", # SQLAlchemy TypeCompiler requires visit_UPPERCASE method names
# Missing docstrings (#882)
"D100",
"D102",
"D107",
]
"pyathena/aio/sqlalchemy/base.py" = [
"N801", # SQLAlchemy async adapter naming convention (AsyncAdapt_*)
# Missing docstrings (#882)
"D100",
"D102",
"D107",
]
# Missing docstrings (#882). Remove a file's D codes once it is documented.
"pyathena/__init__.py" = ["D104"]
"pyathena/aio/__init__.py" = ["D104"]
"pyathena/aio/arrow/__init__.py" = ["D104"]
"pyathena/aio/arrow/cursor.py" = ["D100", "D107"]
"pyathena/aio/common.py" = ["D100"]
"pyathena/aio/connection.py" = ["D100", "D107"]
"pyathena/aio/cursor.py" = ["D100", "D107"]
"pyathena/aio/pandas/__init__.py" = ["D104"]
"pyathena/aio/pandas/cursor.py" = ["D100", "D107"]
"pyathena/aio/polars/__init__.py" = ["D104"]
"pyathena/aio/polars/cursor.py" = ["D100", "D107"]
"pyathena/aio/result_set.py" = ["D100", "D107"]
"pyathena/aio/s3fs/__init__.py" = ["D104"]
"pyathena/aio/s3fs/cursor.py" = ["D100", "D107"]
"pyathena/aio/spark/__init__.py" = ["D104"]
"pyathena/aio/spark/cursor.py" = ["D100"]
"pyathena/aio/sqlalchemy/__init__.py" = ["D104"]
"pyathena/aio/sqlalchemy/arrow.py" = ["D100"]
"pyathena/aio/sqlalchemy/pandas.py" = ["D100"]
"pyathena/aio/sqlalchemy/polars.py" = ["D100"]
"pyathena/aio/sqlalchemy/rest.py" = ["D100"]
"pyathena/aio/sqlalchemy/s3fs.py" = ["D100"]
"pyathena/aio/util.py" = ["D100"]
"pyathena/arrow/__init__.py" = ["D104"]
"pyathena/arrow/async_cursor.py" = ["D100"]
"pyathena/arrow/converter.py" = ["D100", "D107"]
"pyathena/arrow/cursor.py" = ["D100"]
"pyathena/arrow/result_set.py" = ["D100", "D102", "D107"]
"pyathena/async_cursor.py" = ["D100", "D102", "D107"]
"pyathena/common.py" = ["D100", "D102", "D107"]
"pyathena/connection.py" = ["D100"]
"pyathena/converter.py" = ["D100", "D102", "D107"]
"pyathena/cursor.py" = ["D100", "D107"]
"pyathena/error.py" = ["D100"]
"pyathena/filesystem/__init__.py" = ["D104"]
"pyathena/filesystem/s3.py" = ["D100", "D102", "D107"]
"pyathena/filesystem/s3_async.py" = ["D100", "D102", "D107"]
"pyathena/filesystem/s3_errors.py" = ["D107"]
"pyathena/filesystem/s3_executor.py" = ["D100", "D107"]
"pyathena/filesystem/s3_object.py" = ["D100", "D102", "D107"]
"pyathena/formatter.py" = ["D100", "D102", "D107"]
"pyathena/glue.py" = ["D107"]
"pyathena/model.py" = ["D100", "D102", "D107"]
"pyathena/options.py" = ["D100"]
"pyathena/pandas/__init__.py" = ["D104"]
"pyathena/pandas/async_cursor.py" = ["D100", "D107"]
"pyathena/pandas/converter.py" = ["D100", "D107"]
"pyathena/pandas/cursor.py" = ["D100"]
"pyathena/pandas/reader.py" = ["D100", "D107"]
"pyathena/pandas/result_set.py" = ["D100", "D102"]
"pyathena/pandas/util.py" = ["D100"]
"pyathena/parser.py" = ["D100", "D107"]
"pyathena/polars/__init__.py" = ["D104"]
"pyathena/polars/async_cursor.py" = ["D100"]
"pyathena/polars/converter.py" = ["D100", "D107"]
"pyathena/polars/cursor.py" = ["D100"]
"pyathena/polars/result_set.py" = ["D100"]
"pyathena/result_set.py" = ["D100", "D102", "D107"]
"pyathena/s3fs/__init__.py" = ["D104"]
"pyathena/s3fs/async_cursor.py" = ["D100"]
"pyathena/s3fs/converter.py" = ["D100", "D107"]
"pyathena/s3fs/cursor.py" = ["D100"]
"pyathena/s3fs/reader.py" = ["D100"]
"pyathena/s3fs/result_set.py" = ["D100", "D107"]
"pyathena/spark/__init__.py" = ["D104"]
"pyathena/spark/async_cursor.py" = ["D100", "D102"]
"pyathena/spark/common.py" = ["D100", "D102", "D107"]
"pyathena/spark/cursor.py" = ["D100", "D102"]
"pyathena/sqlalchemy/__init__.py" = ["D104"]
"pyathena/sqlalchemy/array.py" = ["D107"]
"pyathena/sqlalchemy/arrow.py" = ["D100"]
"pyathena/sqlalchemy/base.py" = ["D100", "D107"]
"pyathena/sqlalchemy/map.py" = ["D107"]
"pyathena/sqlalchemy/pandas.py" = ["D100"]
"pyathena/sqlalchemy/polars.py" = ["D100"]
"pyathena/sqlalchemy/preparer.py" = ["D100", "D107"]
"pyathena/sqlalchemy/requirements.py" = ["D100"]
"pyathena/sqlalchemy/rest.py" = ["D100"]
"pyathena/sqlalchemy/s3fs.py" = ["D100"]
"pyathena/sqlalchemy/struct.py" = ["D107"]
"pyathena/util.py" = ["D100", "D107"]

[tool.mypy]
follow_imports = "silent"
Expand Down
Loading