docstring-tailor 0.4.0__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/.gitignore +0 -2
- docstring_tailor-0.4.1/.pre-commit-hooks.yaml +8 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/PKG-INFO +62 -9
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/README.md +60 -7
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/pyproject.toml +1 -1
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/constants.py +4 -12
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/ir_model.py +5 -2
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/main.py +170 -22
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/parser/indentation_based/numpy_docstring_parser.py +3 -2
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/parser/indentation_based/structured_list_parser.py +35 -19
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/renderer/base_renderer.py +5 -1
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/renderer/google_renderer.py +3 -1
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/renderer/numpy_renderer.py +9 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/renderer/sphinx_renderer.py +8 -4
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/utils/utils_parsing.py +8 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/cases/formatting_cases.py +91 -1
- docstring_tailor-0.4.1/tests/fixtures/google/docstring_elements/structured_list_malformed/structured_list_malformed_80.py +11 -0
- docstring_tailor-0.4.1/tests/fixtures/google/docstring_elements/structured_list_malformed/structured_list_malformed_wrong_input.py +12 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/named_paragraph_code_block/named_paragraph_code_block_80.py +8 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/named_paragraph_code_block/named_paragraph_code_block_blank_lines.py +14 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/named_paragraph_code_repl/named_paragraph_code_repl_80.py +26 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/named_paragraph_code_repl/named_paragraph_code_repl_blank_lines.py +37 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/named_paragraph_paragraph/named_paragraph_paragraph_100.py +7 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/named_paragraph_paragraph/named_paragraph_paragraph_60.py +9 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/named_paragraph_paragraph/named_paragraph_paragraph_80.py +8 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/named_paragraph_paragraph/named_paragraph_paragraph_wrong_input.py +24 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/paragraph_and_simple_list/paragraph_and_simple_list_100.py +23 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/paragraph_and_simple_list/paragraph_and_simple_list_60.py +31 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/paragraph_and_simple_list/paragraph_and_simple_list_80.py +25 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/paragraph_and_simple_list/paragraph_and_simple_list_wrong_input.py +41 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/structured_list/structured_list_100.py +37 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/structured_list/structured_list_60.py +44 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/structured_list/structured_list_80.py +38 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/structured_list/structured_list_wrong_input.py +90 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/structured_list_malformed/structured_list_malformed_80.py +17 -0
- docstring_tailor-0.4.1/tests/fixtures/numpy/docstring_elements/structured_list_malformed/structured_list_malformed_wrong_input.py +17 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/uv.lock +1 -1
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/LICENSE +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/makefile +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/__init__.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/cli_config.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/docstring_visitor.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/parser/directive_based/sphinx_docstring_parser.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/parser/docstring_parser_base.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/parser/indentation_based/google_docstring_parser.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/parser/indentation_based/indentation_based_parser.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/parser/parser_factory.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/renderer/renderer_factory.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/utils/__init__.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/utils/utils_cli.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/utils/utils_file_system.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/utils/utils_formatting.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/utils/utils_keyword_translation.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/utils/utils_list_detection.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/src/docstring_tailor/utils/utils_printing.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/cases/__init__.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/cases/config_model.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/class_docstring/class_docstring_100.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/class_docstring/class_docstring_60.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/class_docstring/class_docstring_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/code_block_multiple/code_block_multiple_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/code_block_multiple/code_block_multiple_blank_lines.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/code_block_singular/code_block_singular_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/code_block_singular/code_block_singular_blank_lines.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/code_repl_multiple/code_repl_multiple_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/code_repl_multiple/code_repl_multiple_blank_lines.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/code_repl_singular/code_repl_singular_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/code_repl_singular/code_repl_singular_blank_lines.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/empty/empty_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/empty/empty_blank_lines.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/empty/empty_no_space.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/named_paragraph_code_block/named_paragraph_code_block_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/named_paragraph_code_block/named_paragraph_code_block_blank_lines.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/named_paragraph_code_repl/named_paragraph_code_repl_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/named_paragraph_code_repl/named_paragraph_code_repl_blank_lines.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/named_paragraph_paragraph/named_paragraph_paragraph_100.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/named_paragraph_paragraph/named_paragraph_paragraph_60.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/named_paragraph_paragraph/named_paragraph_paragraph_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/named_paragraph_paragraph/named_paragraph_paragraph_wrong_input.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/paragraph_and_simple_list/paragraph_and_simple_list_100.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/paragraph_and_simple_list/paragraph_and_simple_list_60.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/paragraph_and_simple_list/paragraph_and_simple_list_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/paragraph_and_simple_list/paragraph_and_simple_list_wrong_input.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/paragraph_multi_line/paragraph_multi_line_100.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/paragraph_multi_line/paragraph_multi_line_60.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/paragraph_multi_line/paragraph_multi_line_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/paragraph_multi_line/paragraph_multi_line_wrong_input.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/paragraph_one_line/paragraph_one_line_100.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/paragraph_one_line/paragraph_one_line_60.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/paragraph_one_line/paragraph_one_line_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/paragraph_one_line/paragraph_one_line_wrong_input.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/simple_list/simple_list_100.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/simple_list/simple_list_60.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/simple_list/simple_list_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/simple_list/simple_list_wrong_input.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/structured_list/structured_list_100.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/structured_list/structured_list_60.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/structured_list/structured_list_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/docstring_elements/structured_list/structured_list_wrong_input.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/function_docstring/function_docstring_100.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/function_docstring/function_docstring_60.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/function_docstring/function_docstring_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/module_docstring/module_docstring_100.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/module_docstring/module_docstring_60.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/module_docstring/module_docstring_80.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/fixtures/google/readme_examples/readme_examples.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/test_docstring_tailor.py +0 -0
- {docstring_tailor-0.4.0 → docstring_tailor-0.4.1}/tests/utils/utils_testing.py +0 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Hook definition consumed via `repo: https://github.com/AukeB/docstring-tailor`.
|
|
2
|
+
# Consumers must supply the style through `args`, e.g. args: ["style", "google"].
|
|
3
|
+
- id: docstring-tailor
|
|
4
|
+
name: docstring-tailor
|
|
5
|
+
description: Format Python docstrings to a configurable line length and style.
|
|
6
|
+
entry: docstring_tailor format
|
|
7
|
+
language: python
|
|
8
|
+
types: [python]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: docstring-tailor
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Formats Python docstrings to PEP 257 style with configurable line length.
|
|
5
5
|
Author-email: Auke Bruinsma <afbruinsma@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -44,16 +44,17 @@ Formats Python docstrings to PEP 257 style with configurable line length.
|
|
|
44
44
|
## Table of Contents
|
|
45
45
|
1. [Demo](#demo)
|
|
46
46
|
2. [Installation](#Installation)
|
|
47
|
-
3. [Quick start](#
|
|
47
|
+
3. [Quick start](#quick-start)
|
|
48
48
|
4. [API Overview](#api-overview)
|
|
49
49
|
- [Command](#command)
|
|
50
50
|
- [Options](#options)
|
|
51
51
|
- [Examples](#examples)
|
|
52
|
-
5. [
|
|
53
|
-
6. [
|
|
54
|
-
7. [
|
|
55
|
-
8. [
|
|
56
|
-
9. [
|
|
52
|
+
5. [Pre-commit and prek hook](#pre-commit-and-prek-hook)
|
|
53
|
+
6. [Example docstrings](#example-docstrings)
|
|
54
|
+
7. [What Your Line Length Says About You!](#what-your-line-length-says-about-you)
|
|
55
|
+
8. [Resources](#resources)
|
|
56
|
+
9. [Release Notes](#release-notes)
|
|
57
|
+
10. [Roadmap](#roadmap)
|
|
57
58
|
|
|
58
59
|
## Demo
|
|
59
60
|
|
|
@@ -135,6 +136,8 @@ To convert existing docstrings from one style to another, use the `convert` comm
|
|
|
135
136
|
uv run docstring_tailor convert my_file.py --from-style google --to-style numpy
|
|
136
137
|
```
|
|
137
138
|
|
|
139
|
+
`docstring-tailor` can also run as a [pre-commit](https://pre-commit.com/) / [prek](https://github.com/j178/prek) hook so docstrings stay formatted on every commit, see [Pre-commit and prek hook](#pre-commit-and-prek-hook).
|
|
140
|
+
|
|
138
141
|
## API Overview
|
|
139
142
|
|
|
140
143
|
### Commands
|
|
@@ -217,6 +220,56 @@ style = "google"
|
|
|
217
220
|
exclude = ["tests", "src/generated/*.py"]
|
|
218
221
|
```
|
|
219
222
|
|
|
223
|
+
## Pre-commit and prek hook
|
|
224
|
+
|
|
225
|
+
`docstring-tailor` can run as a [pre-commit](https://pre-commit.com/) hook, and works unchanged with [prek](https://github.com/j178/prek), the drop-in reimplementation, so docstrings stay formatted automatically on every commit. When the hook reformats a file it rewrites it in place and stops the commit so you can review and re-stage the change, exactly like the Ruff or Black hooks.
|
|
226
|
+
|
|
227
|
+
### Hosted hook
|
|
228
|
+
|
|
229
|
+
Add the following to your `.pre-commit-config.yaml`:
|
|
230
|
+
|
|
231
|
+
```yaml
|
|
232
|
+
repos:
|
|
233
|
+
- repo: https://github.com/AukeB/docstring-tailor
|
|
234
|
+
rev: 0.4.1
|
|
235
|
+
hooks:
|
|
236
|
+
- id: docstring-tailor
|
|
237
|
+
args: ["--style", "google"]
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
- `--style` is required, use `google`, `numpy` or `sphinx`. Pass any other option (`--line-length`, `--exclude`) through `args`, or set them in a `[tool.docstring_tailor]` config block instead. `args` is a single flat list where every flag and every value is its own quoted element (e.g. `["--style", "google", "--line-length", "88"]`, with numbers quoted). Install the hook once with `pre-commit install` (or `prek install`).
|
|
241
|
+
|
|
242
|
+
Prefer TOML? prek also reads a native `prek.toml`, a drop-in alternative to `.pre-commit-config.yaml` (upstream `pre-commit` ignores it). The same hook in `prek.toml`
|
|
243
|
+
|
|
244
|
+
```toml
|
|
245
|
+
[[repos]]
|
|
246
|
+
repo = "https://github.com/AukeB/docstring-tailor"
|
|
247
|
+
rev = "v0.4.1"
|
|
248
|
+
hooks= [
|
|
249
|
+
{ id = "docstring-tailor", args = ["--style", "google"] },
|
|
250
|
+
]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`prek util yaml-to-toml` converts an existing YAML config for you.
|
|
254
|
+
|
|
255
|
+
### Local hook
|
|
256
|
+
|
|
257
|
+
If your project already installs `docstring-tailor` (for example as a `uv` dev dependency), you can run it as a `repo: local` hook. pre-commit then never clones a remote repo, which is handy in locked-down or offline environments:
|
|
258
|
+
|
|
259
|
+
```yaml
|
|
260
|
+
repos:
|
|
261
|
+
- repo: local
|
|
262
|
+
hooks:
|
|
263
|
+
- id: docstring-tailor
|
|
264
|
+
name: docstring-tailor
|
|
265
|
+
entry: uv run docstring_tailor format --style google
|
|
266
|
+
language: system
|
|
267
|
+
types: [python]
|
|
268
|
+
pass_filenames: true
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
For CI, run `docstring-tailor format --diff` instead: it never writes files and exits non-zero when any docstring would change, so it can gate a pipeline.
|
|
272
|
+
|
|
220
273
|
## Example docstrings
|
|
221
274
|
|
|
222
275
|
1. [Module docstring](#module-docstring)
|
|
@@ -654,6 +707,7 @@ You don't wrap text because wrapping is a skill issue. You let photons travel un
|
|
|
654
707
|
| `0.3.0` | 2026-07-20 | Feature update & bug fixes | <ul><li>Introduced a style-agnostic intermediate representation (IR) model and refactored parsing into an abstract base class hierarchy. Google and NumPy docstrings now parse into the same IR via `IndentationBasedParser` subclasses, enabling lossless conversion between styles. Structured list parsing is delegated to style-specific implementations to handle syntactic differences (Google's inline `name (type):` vs. NumPy's `name : type` on separate lines).</li><li>Refactored rendering to match the parser architecture: `DocstringRendererBase` (ABC) with style-specific subclasses that implement two hooks — section header formatting (Google's `Args:` vs. NumPy's `Parameters` + underline) and section body indentation rules (confirmed against numpy's own docstrings to keep bodies flush with headers, unlike Google). Keyword translation between styles happens automatically during conversion.</li><li>Code sections and Python REPL blocks now also can be created outside the 'Example(s)' section.</li><li>Fixed a bug when codeblock sections contain blank lines.</li><li>Fixed a bug when the docstring starts immediately with an (un)ordered list.</li><li>Removed `detect-lists` as CLI parameter, because the logic should always be applied if the docstring contains (un)ordered lists.</li><li>Changed behaviour for empty docstrings so that it consistent with Ruff.</li></ul> |
|
|
655
708
|
| `0.3.1` | 2026-07-21 | Small fixes | <ul><li>The `style` parameter does not have a default argument anymore, instead it always has to be configured explicitly by the user.</li><li>Fixed a bug where one-line docstrings inside indented scopes (e.g. class or method bodies) were wrapped to multiple lines prematurely, due to indentation being counted twice when checking against the configured line length.</li> <li>Added an exception, consistent with Ruff, allowing a one-line docstring to exceed the configured line length by up to 3 characters when only the closing triple quotes would otherwise be pushed onto their own line. This prevents docstring_tailor and Ruff from repeatedly reformatting the same docstring back and forth when both are run as pre-commit hooks.</li></ul> |
|
|
656
709
|
| `0.4.0` | 2026-07-24 | Feature update | <ul><li>Added support for the Sphinx/reST docstring `style`, covering both parsing and rendering. Sphinx docstrings now parse into the same style-agnostic intermediate representation (IR) as Google and NumPy, enabling lossless conversion in all directions between the three styles (`format --style sphinx` and `convert` to/from `sphinx`).</li><li>Extracted a shared `DocstringParserBase` holding the style-agnostic flat-content pipeline; `IndentationBasedParser` (Google/NumPy) and the new directive-based `SphinxDocstringParser` both build on it, reusing the existing pipeline rather than duplicating logic.</li><li>The Sphinx parser handles info-field lists (`:param:`/`:type:`, `:returns:`/`:rtype:`, `:raises:`), the inline parameter form (`:param str name:`), field/type pairing by name, tag aliases (`:return:`, `:raise:`, `:except:`, `:exception:`), and informational directives (`.. note::`, `.. warning::`, `.. seealso::`, `.. example::`).</li><li>The Sphinx renderer emits a contiguous info-field list with separate `:type:`/`:rtype:` lines, with hanging-indent wrapping of field bodies.</li><li>Modelled an absent parameter type as `None` in the IR (never fabricated), and updated the Google and NumPy renderers to degrade gracefully when a type is undocumented.</li><li>Reorganized the golden-file test fixtures into per-style folders (`google/`, `sphinx/`, `convert/`), with the fixture style folder derived automatically from each test case's configuration.</li></ul> |
|
|
710
|
+
| `0.4.1` | 2026-09-12 | Robustness & bug fixes | <ul><li> Made structured-list parsing resilient to malformed entries: a missing `:` separator or a missing `(type)` annotation no longer crashes. The entry's text is preserved (with name and type left unset) and rendered as written, so formatting never fails on imperfect docstrings.</li><li> Modelled a `Raises` entry that lacks a `:` as an unnamed error (`error_type` set to `None`) in the IR, consistent with how unclassifiable parameter entries are handled, and guarded the Google, Numpy and Sphinx renderes accordingly.</li><li> The CLI now processes each file independently: files that cannot be read, decoded, or parsed as Python are reported with a specific error message and skipped, so a single malformed file no longer aborts the whole run.</li><li> Added a Ruff-style run summary (e.g. `2 files reformatted, 1 file left unchanged`) and stopped rewriting files whose content did not change.</li><li>Aligned exit codes with Ruff: `format` exits non-zero only on errors (write mode) or when changes are needed (`--diff`), and `convert` exits non-zero only on errors, enabling reliable use in pre-commit and CI.</li><li>Removed a leftover debug `print` from the CLI output.</li><li> Published a `pre-commit-config.yaml` hook definition so `docstring-tailor` can run as a pre-commit / prek hook via `repo:`, with hosted, local, and `prek.toml` setups documented in the README </li></ul> |
|
|
657
711
|
|
|
658
712
|
## Roadmap
|
|
659
713
|
|
|
@@ -681,7 +735,6 @@ The core Sphinx style (parameters, returns, raises, and the common admonitions)
|
|
|
681
735
|
- Literal-body directives (`.. code-block::`, `.. math::`) — more involved, because their content must be preserved verbatim and never reflowed, and there is no clean equivalent in the current IR. Needs a dedicated literal-block IR node before it can be handled safely.
|
|
682
736
|
|
|
683
737
|
### Nice to have
|
|
684
|
-
- Make sure the package can be used as a pre-commit hook.
|
|
685
738
|
- LSP (Language Server Protocol) support, enabling real-time feedback on malformed
|
|
686
739
|
docstrings directly in editors like VS Code, PyCharm and Neovim. Built on top of
|
|
687
740
|
the validation layer and the existing parser, with `pygls` handling the protocol.
|
|
@@ -11,16 +11,17 @@ Formats Python docstrings to PEP 257 style with configurable line length.
|
|
|
11
11
|
## Table of Contents
|
|
12
12
|
1. [Demo](#demo)
|
|
13
13
|
2. [Installation](#Installation)
|
|
14
|
-
3. [Quick start](#
|
|
14
|
+
3. [Quick start](#quick-start)
|
|
15
15
|
4. [API Overview](#api-overview)
|
|
16
16
|
- [Command](#command)
|
|
17
17
|
- [Options](#options)
|
|
18
18
|
- [Examples](#examples)
|
|
19
|
-
5. [
|
|
20
|
-
6. [
|
|
21
|
-
7. [
|
|
22
|
-
8. [
|
|
23
|
-
9. [
|
|
19
|
+
5. [Pre-commit and prek hook](#pre-commit-and-prek-hook)
|
|
20
|
+
6. [Example docstrings](#example-docstrings)
|
|
21
|
+
7. [What Your Line Length Says About You!](#what-your-line-length-says-about-you)
|
|
22
|
+
8. [Resources](#resources)
|
|
23
|
+
9. [Release Notes](#release-notes)
|
|
24
|
+
10. [Roadmap](#roadmap)
|
|
24
25
|
|
|
25
26
|
## Demo
|
|
26
27
|
|
|
@@ -102,6 +103,8 @@ To convert existing docstrings from one style to another, use the `convert` comm
|
|
|
102
103
|
uv run docstring_tailor convert my_file.py --from-style google --to-style numpy
|
|
103
104
|
```
|
|
104
105
|
|
|
106
|
+
`docstring-tailor` can also run as a [pre-commit](https://pre-commit.com/) / [prek](https://github.com/j178/prek) hook so docstrings stay formatted on every commit, see [Pre-commit and prek hook](#pre-commit-and-prek-hook).
|
|
107
|
+
|
|
105
108
|
## API Overview
|
|
106
109
|
|
|
107
110
|
### Commands
|
|
@@ -184,6 +187,56 @@ style = "google"
|
|
|
184
187
|
exclude = ["tests", "src/generated/*.py"]
|
|
185
188
|
```
|
|
186
189
|
|
|
190
|
+
## Pre-commit and prek hook
|
|
191
|
+
|
|
192
|
+
`docstring-tailor` can run as a [pre-commit](https://pre-commit.com/) hook, and works unchanged with [prek](https://github.com/j178/prek), the drop-in reimplementation, so docstrings stay formatted automatically on every commit. When the hook reformats a file it rewrites it in place and stops the commit so you can review and re-stage the change, exactly like the Ruff or Black hooks.
|
|
193
|
+
|
|
194
|
+
### Hosted hook
|
|
195
|
+
|
|
196
|
+
Add the following to your `.pre-commit-config.yaml`:
|
|
197
|
+
|
|
198
|
+
```yaml
|
|
199
|
+
repos:
|
|
200
|
+
- repo: https://github.com/AukeB/docstring-tailor
|
|
201
|
+
rev: 0.4.1
|
|
202
|
+
hooks:
|
|
203
|
+
- id: docstring-tailor
|
|
204
|
+
args: ["--style", "google"]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
- `--style` is required, use `google`, `numpy` or `sphinx`. Pass any other option (`--line-length`, `--exclude`) through `args`, or set them in a `[tool.docstring_tailor]` config block instead. `args` is a single flat list where every flag and every value is its own quoted element (e.g. `["--style", "google", "--line-length", "88"]`, with numbers quoted). Install the hook once with `pre-commit install` (or `prek install`).
|
|
208
|
+
|
|
209
|
+
Prefer TOML? prek also reads a native `prek.toml`, a drop-in alternative to `.pre-commit-config.yaml` (upstream `pre-commit` ignores it). The same hook in `prek.toml`
|
|
210
|
+
|
|
211
|
+
```toml
|
|
212
|
+
[[repos]]
|
|
213
|
+
repo = "https://github.com/AukeB/docstring-tailor"
|
|
214
|
+
rev = "v0.4.1"
|
|
215
|
+
hooks= [
|
|
216
|
+
{ id = "docstring-tailor", args = ["--style", "google"] },
|
|
217
|
+
]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`prek util yaml-to-toml` converts an existing YAML config for you.
|
|
221
|
+
|
|
222
|
+
### Local hook
|
|
223
|
+
|
|
224
|
+
If your project already installs `docstring-tailor` (for example as a `uv` dev dependency), you can run it as a `repo: local` hook. pre-commit then never clones a remote repo, which is handy in locked-down or offline environments:
|
|
225
|
+
|
|
226
|
+
```yaml
|
|
227
|
+
repos:
|
|
228
|
+
- repo: local
|
|
229
|
+
hooks:
|
|
230
|
+
- id: docstring-tailor
|
|
231
|
+
name: docstring-tailor
|
|
232
|
+
entry: uv run docstring_tailor format --style google
|
|
233
|
+
language: system
|
|
234
|
+
types: [python]
|
|
235
|
+
pass_filenames: true
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
For CI, run `docstring-tailor format --diff` instead: it never writes files and exits non-zero when any docstring would change, so it can gate a pipeline.
|
|
239
|
+
|
|
187
240
|
## Example docstrings
|
|
188
241
|
|
|
189
242
|
1. [Module docstring](#module-docstring)
|
|
@@ -621,6 +674,7 @@ You don't wrap text because wrapping is a skill issue. You let photons travel un
|
|
|
621
674
|
| `0.3.0` | 2026-07-20 | Feature update & bug fixes | <ul><li>Introduced a style-agnostic intermediate representation (IR) model and refactored parsing into an abstract base class hierarchy. Google and NumPy docstrings now parse into the same IR via `IndentationBasedParser` subclasses, enabling lossless conversion between styles. Structured list parsing is delegated to style-specific implementations to handle syntactic differences (Google's inline `name (type):` vs. NumPy's `name : type` on separate lines).</li><li>Refactored rendering to match the parser architecture: `DocstringRendererBase` (ABC) with style-specific subclasses that implement two hooks — section header formatting (Google's `Args:` vs. NumPy's `Parameters` + underline) and section body indentation rules (confirmed against numpy's own docstrings to keep bodies flush with headers, unlike Google). Keyword translation between styles happens automatically during conversion.</li><li>Code sections and Python REPL blocks now also can be created outside the 'Example(s)' section.</li><li>Fixed a bug when codeblock sections contain blank lines.</li><li>Fixed a bug when the docstring starts immediately with an (un)ordered list.</li><li>Removed `detect-lists` as CLI parameter, because the logic should always be applied if the docstring contains (un)ordered lists.</li><li>Changed behaviour for empty docstrings so that it consistent with Ruff.</li></ul> |
|
|
622
675
|
| `0.3.1` | 2026-07-21 | Small fixes | <ul><li>The `style` parameter does not have a default argument anymore, instead it always has to be configured explicitly by the user.</li><li>Fixed a bug where one-line docstrings inside indented scopes (e.g. class or method bodies) were wrapped to multiple lines prematurely, due to indentation being counted twice when checking against the configured line length.</li> <li>Added an exception, consistent with Ruff, allowing a one-line docstring to exceed the configured line length by up to 3 characters when only the closing triple quotes would otherwise be pushed onto their own line. This prevents docstring_tailor and Ruff from repeatedly reformatting the same docstring back and forth when both are run as pre-commit hooks.</li></ul> |
|
|
623
676
|
| `0.4.0` | 2026-07-24 | Feature update | <ul><li>Added support for the Sphinx/reST docstring `style`, covering both parsing and rendering. Sphinx docstrings now parse into the same style-agnostic intermediate representation (IR) as Google and NumPy, enabling lossless conversion in all directions between the three styles (`format --style sphinx` and `convert` to/from `sphinx`).</li><li>Extracted a shared `DocstringParserBase` holding the style-agnostic flat-content pipeline; `IndentationBasedParser` (Google/NumPy) and the new directive-based `SphinxDocstringParser` both build on it, reusing the existing pipeline rather than duplicating logic.</li><li>The Sphinx parser handles info-field lists (`:param:`/`:type:`, `:returns:`/`:rtype:`, `:raises:`), the inline parameter form (`:param str name:`), field/type pairing by name, tag aliases (`:return:`, `:raise:`, `:except:`, `:exception:`), and informational directives (`.. note::`, `.. warning::`, `.. seealso::`, `.. example::`).</li><li>The Sphinx renderer emits a contiguous info-field list with separate `:type:`/`:rtype:` lines, with hanging-indent wrapping of field bodies.</li><li>Modelled an absent parameter type as `None` in the IR (never fabricated), and updated the Google and NumPy renderers to degrade gracefully when a type is undocumented.</li><li>Reorganized the golden-file test fixtures into per-style folders (`google/`, `sphinx/`, `convert/`), with the fixture style folder derived automatically from each test case's configuration.</li></ul> |
|
|
677
|
+
| `0.4.1` | 2026-09-12 | Robustness & bug fixes | <ul><li> Made structured-list parsing resilient to malformed entries: a missing `:` separator or a missing `(type)` annotation no longer crashes. The entry's text is preserved (with name and type left unset) and rendered as written, so formatting never fails on imperfect docstrings.</li><li> Modelled a `Raises` entry that lacks a `:` as an unnamed error (`error_type` set to `None`) in the IR, consistent with how unclassifiable parameter entries are handled, and guarded the Google, Numpy and Sphinx renderes accordingly.</li><li> The CLI now processes each file independently: files that cannot be read, decoded, or parsed as Python are reported with a specific error message and skipped, so a single malformed file no longer aborts the whole run.</li><li> Added a Ruff-style run summary (e.g. `2 files reformatted, 1 file left unchanged`) and stopped rewriting files whose content did not change.</li><li>Aligned exit codes with Ruff: `format` exits non-zero only on errors (write mode) or when changes are needed (`--diff`), and `convert` exits non-zero only on errors, enabling reliable use in pre-commit and CI.</li><li>Removed a leftover debug `print` from the CLI output.</li><li> Published a `pre-commit-config.yaml` hook definition so `docstring-tailor` can run as a pre-commit / prek hook via `repo:`, with hosted, local, and `prek.toml` setups documented in the README </li></ul> |
|
|
624
678
|
|
|
625
679
|
## Roadmap
|
|
626
680
|
|
|
@@ -648,7 +702,6 @@ The core Sphinx style (parameters, returns, raises, and the common admonitions)
|
|
|
648
702
|
- Literal-body directives (`.. code-block::`, `.. math::`) — more involved, because their content must be preserved verbatim and never reflowed, and there is no clean equivalent in the current IR. Needs a dedicated literal-block IR node before it can be handled safely.
|
|
649
703
|
|
|
650
704
|
### Nice to have
|
|
651
|
-
- Make sure the package can be used as a pre-commit hook.
|
|
652
705
|
- LSP (Language Server Protocol) support, enabling real-time feedback on malformed
|
|
653
706
|
docstrings directly in editors like VS Code, PyCharm and Neovim. Built on top of
|
|
654
707
|
the validation layer and the existing parser, with `pygls` handling the protocol.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "docstring-tailor"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.1"
|
|
4
4
|
description = "Formats Python docstrings to PEP 257 style with configurable line length."
|
|
5
5
|
authors = [{ name = "Auke Bruinsma", email = "afbruinsma@gmail.com" }]
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -54,17 +54,6 @@ UNORDERED_LIST_MARKER: str = "- "
|
|
|
54
54
|
ORDERED_LIST_SEPARATOR = ". "
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
# ====================================================
|
|
58
|
-
# Constants used for multiple docstings (but not all).
|
|
59
|
-
# ====================================================
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# Related to StructuredList sections (Use in Google and Numpy format)
|
|
63
|
-
STRUCTURED_LIST_DESCRIPTION_SEPARATOR: str = ":"
|
|
64
|
-
PARAMETER_TYPE_ANNOTATION_OPEN: str = "("
|
|
65
|
-
PARAMETER_TYPE_ANNOTATION_CLOSE: str = ")"
|
|
66
|
-
|
|
67
|
-
|
|
68
57
|
# ============================================
|
|
69
58
|
# Constants used for single docstring formats.
|
|
70
59
|
# ============================================
|
|
@@ -93,6 +82,8 @@ GOOGLE_ALL_SECTION_KEYWORDS = (
|
|
|
93
82
|
GOOGLE_NAMED_PARAGRAPH_SECTIONS | GOOGLE_STRUCTURED_LIST_SECTIONS
|
|
94
83
|
)
|
|
95
84
|
|
|
85
|
+
GOOGLE_STRUCTURED_LIST_DESCRIPTION_SEPARATOR: str = ":"
|
|
86
|
+
|
|
96
87
|
# === NumPy ---
|
|
97
88
|
|
|
98
89
|
NUMPY_ITEM_SECTIONS = frozenset(
|
|
@@ -101,6 +92,7 @@ NUMPY_ITEM_SECTIONS = frozenset(
|
|
|
101
92
|
NUMPY_PLAIN_SECTIONS = frozenset({"Examples", "Notes", "References", "See Also"})
|
|
102
93
|
NUMPY_SECTION_HEADERS = NUMPY_ITEM_SECTIONS | NUMPY_PLAIN_SECTIONS
|
|
103
94
|
|
|
95
|
+
NUMPY_STRUCTURED_LIST_NAME_TYPE_SEPARATOR: str = ":"
|
|
104
96
|
|
|
105
97
|
# Sphinx/reST-style
|
|
106
98
|
SPHINX_ITEM_DIRECTIVES = frozenset({":param", ":raises", ":returns", ":rtype", ":type"})
|
|
@@ -115,7 +107,7 @@ SPHINX_DIRECTIVES = SPHINX_ITEM_DIRECTIVES | SPHINX_PLAIN_DIRECTIVES
|
|
|
115
107
|
# and plural spellings) are accepted on input; the renderer emits one canonical
|
|
116
108
|
# spelling per group. ':param' also accepts an inline type ':param <type>
|
|
117
109
|
# <name>:', handled by the parser.
|
|
118
|
-
SPHINX_PARAM_TAGS = frozenset({":
|
|
110
|
+
SPHINX_PARAM_TAGS = frozenset({":param", ":parameter", ":arg", ":argument"})
|
|
119
111
|
SPHINX_TYPE_TAGS = frozenset({":type"})
|
|
120
112
|
SPHINX_RETURN_TAGS = frozenset({":return", ":returns"})
|
|
121
113
|
SPHINX_RTYPE_TAGS = frozenset({":rtype"})
|
|
@@ -82,11 +82,14 @@ class StructuredListError:
|
|
|
82
82
|
section.
|
|
83
83
|
|
|
84
84
|
Attributes:
|
|
85
|
-
error_type (str): The exception type being raised
|
|
85
|
+
error_type (str | None): The exception type being raised, or None when
|
|
86
|
+
the entry had no ':' separator and could not be split. The whole
|
|
87
|
+
entry is preserved in description instead, mirroring how an
|
|
88
|
+
unclassifiable parameter entry is handled.
|
|
86
89
|
description (str): The description of when the error is raised.
|
|
87
90
|
"""
|
|
88
91
|
|
|
89
|
-
error_type: str
|
|
92
|
+
error_type: str | None
|
|
90
93
|
description: str
|
|
91
94
|
|
|
92
95
|
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
"""Main module
|
|
2
|
-
|
|
3
|
-
Todo
|
|
4
|
-
|
|
5
|
-
- Fix that for return section the variable name is optional.
|
|
6
|
-
- Fix that the program does not break if brackets can not be found for variable
|
|
7
|
-
types.
|
|
8
|
-
"""
|
|
1
|
+
"""Main module"""
|
|
9
2
|
|
|
10
3
|
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
11
5
|
from pathlib import Path
|
|
12
6
|
from typing import Annotated, Optional
|
|
13
7
|
|
|
@@ -135,35 +129,174 @@ def _resolve_common_options(
|
|
|
135
129
|
return resolved_paths, resolved_line_length, resolved_exclude, file_config
|
|
136
130
|
|
|
137
131
|
|
|
132
|
+
def _pluralize_files(file_count: int) -> str:
|
|
133
|
+
"""Returns the correctly pluralized noun for a file count.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
file_count (int): The number of files.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
noun (str): 'file' when count is 1, 'files' otherwise.
|
|
140
|
+
"""
|
|
141
|
+
noun = "file" if file_count == 1 else "files"
|
|
142
|
+
|
|
143
|
+
return noun
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _format_run_summary(counter_reformatted: int, counter_unchanged: int) -> str:
|
|
147
|
+
"""Builds a Ruff-style one-line summary of a formatting run.
|
|
148
|
+
|
|
149
|
+
Mirrors 'ruff format': only the non-zero categories are named, except when
|
|
150
|
+
nothing was processed at all, in which case both are reported as zero.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
counter_reformatted (int): Number of files whose content changed.
|
|
154
|
+
counter_unchanged (int): Number of files left unchanged.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
summary (str): The human-readable summary line.
|
|
158
|
+
"""
|
|
159
|
+
reformatted_part = f"{counter_reformatted} {_pluralize_files(file_count=counter_reformatted)} reformatted"
|
|
160
|
+
unchanged_part = f"{counter_unchanged} {_pluralize_files(file_count=counter_unchanged)} left unchanged"
|
|
161
|
+
|
|
162
|
+
if counter_reformatted and counter_unchanged:
|
|
163
|
+
format_summary = f"{reformatted_part}, {unchanged_part}"
|
|
164
|
+
elif counter_reformatted:
|
|
165
|
+
format_summary = reformatted_part
|
|
166
|
+
elif counter_unchanged:
|
|
167
|
+
format_summary = unchanged_part
|
|
168
|
+
else:
|
|
169
|
+
format_summary = "0 files reformatted, 0 files left unchanged"
|
|
170
|
+
|
|
171
|
+
return format_summary
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass(frozen=True)
|
|
175
|
+
class _FormatResult:
|
|
176
|
+
"""Outcome counts of a formatting run for multiple python files.
|
|
177
|
+
|
|
178
|
+
Attributes:
|
|
179
|
+
count_reformatted (int): Number of files whose content changed.
|
|
180
|
+
count_unchanged (int): Number of files left untouched.
|
|
181
|
+
count_errored (int): Number of files that could not be read, decoded, or
|
|
182
|
+
parsed and were skipped.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
count_reformatted: int
|
|
186
|
+
count_unchanged: int
|
|
187
|
+
count_errored: int
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _process_single_file(
|
|
191
|
+
file_path: Path,
|
|
192
|
+
visitor_factory: Callable[[], DocstringVisitor],
|
|
193
|
+
diff: bool,
|
|
194
|
+
) -> bool:
|
|
195
|
+
"""Reads, transforms, and writes or diffs a single Python file.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
file_path (Path): The file to process.
|
|
199
|
+
visitor_factory (Callable[[], DocstringVisitor]): Builds a fresh
|
|
200
|
+
DocstringVisitor this file.
|
|
201
|
+
diff (bool): If True, print a diff instead of writing files.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
is_changed (bool): True if formatting changed the file's content.
|
|
205
|
+
"""
|
|
206
|
+
input_data = file_path.read_text(encoding=ENCODING)
|
|
207
|
+
input_tree = cst.parse_module(source=input_data)
|
|
208
|
+
modified_tree = input_tree.visit(visitor_factory())
|
|
209
|
+
|
|
210
|
+
modified_code = modified_tree.code
|
|
211
|
+
is_changed = modified_code != input_data
|
|
212
|
+
|
|
213
|
+
if diff:
|
|
214
|
+
show_diff(original=input_data, modified=modified_code, path=file_path)
|
|
215
|
+
elif is_changed:
|
|
216
|
+
file_path.write_text(modified_code, encoding=ENCODING)
|
|
217
|
+
|
|
218
|
+
return is_changed
|
|
219
|
+
|
|
220
|
+
|
|
138
221
|
def _process_files(
|
|
139
222
|
python_files: list[Path],
|
|
140
223
|
visitor_factory: Callable[[], DocstringVisitor],
|
|
141
224
|
diff: bool,
|
|
142
|
-
) ->
|
|
225
|
+
) -> _FormatResult:
|
|
143
226
|
"""Parses, transforms, and writes or diffs each collected Python file.
|
|
144
227
|
|
|
145
228
|
A fresh DocstringVisitor is created per file via visitor_factory, since
|
|
146
229
|
DocstringVisitor accumulates indentation state as it traverses a single
|
|
147
|
-
file's CST and cannot be safely reused across files.
|
|
230
|
+
file's CST and cannot be safely reused across files. Unchanged files are not
|
|
231
|
+
rewritten, so their on-disk timestamps are preserved. In write mode a Ruff-
|
|
232
|
+
style summary of the run is printed once all files are processed.
|
|
233
|
+
|
|
234
|
+
A file that cannot be read, decoded, or parsed as Python is reported to
|
|
235
|
+
stderr and skipped, so a single malformed file never aborts the whole run
|
|
236
|
+
and every other file is still processed.
|
|
148
237
|
|
|
149
238
|
Args:
|
|
150
239
|
python_files (list[Path]): The collected files to process.
|
|
151
240
|
visitor_factory (Callable[[], DocstringVisitor]): Builds a fresh
|
|
152
241
|
DocstringVisitor for each file.
|
|
153
242
|
diff (bool): If True, print a diff instead of writing files.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
result (_FormatResult): Counts of reformatted, unchanged, and errored
|
|
246
|
+
files, so the caller can choose an exit code.
|
|
247
|
+
|
|
248
|
+
Raises:
|
|
249
|
+
OSError: If the file cannot be read or written.
|
|
250
|
+
UnicodeDecodeError: If the file is not valid text in the expected
|
|
251
|
+
encoding.
|
|
252
|
+
cst.ParserSyntaxError: If the file is not valid python.
|
|
154
253
|
"""
|
|
254
|
+
counter_reformatted: int = 0
|
|
255
|
+
counter_unchanged: int = 0
|
|
256
|
+
counter_errored: int = 0
|
|
257
|
+
|
|
155
258
|
for file_path in python_files:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
259
|
+
try:
|
|
260
|
+
is_changed: bool = _process_single_file(
|
|
261
|
+
file_path=file_path,
|
|
262
|
+
visitor_factory=visitor_factory,
|
|
263
|
+
diff=diff,
|
|
264
|
+
)
|
|
265
|
+
except OSError as error:
|
|
266
|
+
typer.echo(
|
|
267
|
+
f"error: could not read or write '{file_path}': {error}", err=True
|
|
268
|
+
)
|
|
269
|
+
counter_errored += 1
|
|
270
|
+
continue
|
|
271
|
+
except UnicodeDecodeError as error:
|
|
272
|
+
typer.echo(
|
|
273
|
+
f"error: '{file_path}' is not valid {ENCODING} text: {error}", err=True
|
|
274
|
+
)
|
|
275
|
+
counter_errored += 1
|
|
276
|
+
continue
|
|
277
|
+
except cst.ParserSyntaxError as error:
|
|
278
|
+
typer.echo(f"error: '{file_path}' is not valid Python: {error}", err=True)
|
|
279
|
+
counter_errored += 1
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
if is_changed:
|
|
283
|
+
counter_reformatted += 1
|
|
284
|
+
else:
|
|
285
|
+
counter_unchanged += 1
|
|
160
286
|
|
|
161
|
-
|
|
287
|
+
if not diff:
|
|
288
|
+
typer.echo(
|
|
289
|
+
_format_run_summary(
|
|
290
|
+
counter_reformatted=counter_reformatted,
|
|
291
|
+
counter_unchanged=counter_unchanged,
|
|
292
|
+
)
|
|
293
|
+
)
|
|
162
294
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
295
|
+
return _FormatResult(
|
|
296
|
+
count_reformatted=counter_reformatted,
|
|
297
|
+
count_unchanged=counter_unchanged,
|
|
298
|
+
count_errored=counter_errored,
|
|
299
|
+
)
|
|
167
300
|
|
|
168
301
|
|
|
169
302
|
@app.command("format")
|
|
@@ -193,6 +326,11 @@ def format_command(
|
|
|
193
326
|
exclude (list[str] | None): Glob patterns for paths to exclude.
|
|
194
327
|
diff (bool): If True, print a unified diff to stdout instead of writing
|
|
195
328
|
files.
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
typer.Exit: With code 1 if any file could not be processed, or, in
|
|
332
|
+
--diff mode, if any file would be reformatted, so --diff can gate a
|
|
333
|
+
CI run while a normal write run still succeeds after fixing files.
|
|
196
334
|
"""
|
|
197
335
|
resolved_paths, resolved_line_length, resolved_exclude, file_config = (
|
|
198
336
|
_resolve_common_options(paths=paths, line_length=line_length, exclude=exclude)
|
|
@@ -218,7 +356,7 @@ def format_command(
|
|
|
218
356
|
exclude_patterns=resolved_exclude,
|
|
219
357
|
)
|
|
220
358
|
|
|
221
|
-
_process_files(
|
|
359
|
+
format_result = _process_files(
|
|
222
360
|
python_files=python_files,
|
|
223
361
|
visitor_factory=lambda: DocstringVisitor(
|
|
224
362
|
line_length=resolved_line_length,
|
|
@@ -228,6 +366,11 @@ def format_command(
|
|
|
228
366
|
diff=diff,
|
|
229
367
|
)
|
|
230
368
|
|
|
369
|
+
# Write mode treats a reformat as success (files are fixed). Only --diff
|
|
370
|
+
# gates on changes.
|
|
371
|
+
if format_result.count_errored or (diff and format_result.count_reformatted):
|
|
372
|
+
raise typer.Exit(code=1)
|
|
373
|
+
|
|
231
374
|
|
|
232
375
|
@app.command("convert")
|
|
233
376
|
def convert_command(
|
|
@@ -265,8 +408,10 @@ def convert_command(
|
|
|
265
408
|
files.
|
|
266
409
|
|
|
267
410
|
Raises:
|
|
268
|
-
typer.Exit:
|
|
411
|
+
typer.Exit: With code 1 if from_style and to_style are the same.
|
|
412
|
+
typer.Exit: With code 1 if any file could not be processed.
|
|
269
413
|
"""
|
|
414
|
+
|
|
270
415
|
if from_style == to_style:
|
|
271
416
|
typer.echo(
|
|
272
417
|
f"--from-style and --to-style were both '{from_style.value}'. "
|
|
@@ -288,7 +433,7 @@ def convert_command(
|
|
|
288
433
|
exclude_patterns=resolved_exclude,
|
|
289
434
|
)
|
|
290
435
|
|
|
291
|
-
_process_files(
|
|
436
|
+
format_result = _process_files(
|
|
292
437
|
python_files=python_files,
|
|
293
438
|
visitor_factory=lambda: DocstringVisitor(
|
|
294
439
|
line_length=resolved_line_length,
|
|
@@ -298,6 +443,9 @@ def convert_command(
|
|
|
298
443
|
diff=diff,
|
|
299
444
|
)
|
|
300
445
|
|
|
446
|
+
if format_result.count_errored:
|
|
447
|
+
raise typer.Exit(code=1)
|
|
448
|
+
|
|
301
449
|
|
|
302
450
|
if __name__ == "__main__":
|
|
303
451
|
app()
|
|
@@ -85,8 +85,9 @@ class NumpyDocstringParser(IndentationBasedParser):
|
|
|
85
85
|
|
|
86
86
|
next_index = index + 1
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
# Don't think this code is necessary
|
|
89
|
+
# if next_index >= len(lines):
|
|
90
|
+
# return False
|
|
90
91
|
|
|
91
92
|
result = bool(
|
|
92
93
|
RE_PATTERN_NUMPY_SECTION_UNDERLINE.match(lines[next_index].strip())
|