diff --git a/AGENTS.md b/AGENTS.md index d87ab805..51c30048 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/docs/contributing.md b/docs/contributing.md index c0fbac14..5fc5a779 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -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. diff --git a/pyathena/__init__.py b/pyathena/__init__.py index 1e73d195..ee80666d 100644 --- a/pyathena/__init__.py +++ b/pyathena/__init__.py @@ -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 """ @@ -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 @@ -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. diff --git a/pyathena/aio/cursor.py b/pyathena/aio/cursor.py index 38ef885f..addfc6a5 100644 --- a/pyathena/aio/cursor.py +++ b/pyathena/aio/cursor.py @@ -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). diff --git a/pyathena/async_cursor.py b/pyathena/async_cursor.py index 620df9d6..98e351f8 100644 --- a/pyathena/async_cursor.py +++ b/pyathena/async_cursor.py @@ -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). diff --git a/pyathena/common.py b/pyathena/common.py index 963ec459..28a3e0a5 100644 --- a/pyathena/common.py +++ b/pyathena/common.py @@ -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 diff --git a/pyathena/cursor.py b/pyathena/cursor.py index 2e789520..8b91413f 100644 --- a/pyathena/cursor.py +++ b/pyathena/cursor.py @@ -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 diff --git a/pyathena/filesystem/s3.py b/pyathena/filesystem/s3.py index abbe270f..4d29fa14 100644 --- a/pyathena/filesystem/s3.py +++ b/pyathena/filesystem/s3.py @@ -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__( diff --git a/pyathena/result_set.py b/pyathena/result_set.py index dade34f4..45393710 100644 --- a/pyathena/result_set.py +++ b/pyathena/result_set.py @@ -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 diff --git a/pyathena/sqlalchemy/requirements.py b/pyathena/sqlalchemy/requirements.py index 048e9229..a4d9595b 100644 --- a/pyathena/sqlalchemy/requirements.py +++ b/pyathena/sqlalchemy/requirements.py @@ -15,6 +15,8 @@ class Requirements(SuiteRequirements): + """Features of the Athena dialect for the SQLAlchemy test suite.""" + @property @override def comment_reflection(self): diff --git a/pyproject.toml b/pyproject.toml index 414ff78f..40a3530b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] + [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"