docstring-tailor 0.2.0__tar.gz → 0.2.1.dev0__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.
Files changed (46) hide show
  1. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/PKG-INFO +45 -29
  2. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/README.md +44 -28
  3. docstring_tailor-0.2.1.dev0/assets/gif_images/docstring_slider.gif +0 -0
  4. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/pyproject.toml +1 -1
  5. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/src/docstring_tailor/constants.py +4 -0
  6. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/src/docstring_tailor/main.py +55 -28
  7. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/src/docstring_tailor/multi_line_docstring_formatter.py +3 -8
  8. docstring_tailor-0.2.1.dev0/src/docstring_tailor/utils/utils_cli.py +55 -0
  9. docstring_tailor-0.2.1.dev0/src/docstring_tailor/utils/utils_file_system.py +117 -0
  10. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/src/docstring_tailor/utils/utils_formatting.py +0 -85
  11. docstring_tailor-0.2.1.dev0/src/docstring_tailor/utils/utils_list_detection.py +88 -0
  12. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/test_formatting.py +1 -1
  13. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/uv.lock +1 -1
  14. docstring_tailor-0.2.0/src/docstring_tailor/utils/utils_file_system.py +0 -70
  15. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/.gitignore +0 -0
  16. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/LICENSE +0 -0
  17. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/makefile +0 -0
  18. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/src/docstring_tailor/__init__.py +0 -0
  19. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/src/docstring_tailor/cli_config.py +0 -0
  20. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/src/docstring_tailor/docstring_visitor.py +0 -0
  21. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/src/docstring_tailor/utils/__init__.py +0 -0
  22. /docstring_tailor-0.2.0/tests/utils_test.py → /docstring_tailor-0.2.1.dev0/src/docstring_tailor/utils/utils_testing.py +0 -0
  23. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/cases/__init__.py +0 -0
  24. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/cases/config_model.py +0 -0
  25. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/cases/formatting_cases.py +0 -0
  26. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/all_docstrings/all_docstrings_100.py +0 -0
  27. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/all_docstrings/all_docstrings_60.py +0 -0
  28. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/all_docstrings/all_docstrings_80.py +0 -0
  29. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/all_docstrings/all_docstrings_too_long.py +0 -0
  30. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/all_docstrings/all_docstrings_too_short.py +0 -0
  31. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/function_docstring_complex/function_docstring_complex_100.py +0 -0
  32. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/function_docstring_complex/function_docstring_complex_60.py +0 -0
  33. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/function_docstring_complex/function_docstring_complex_80.py +0 -0
  34. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/module_docstring_blank_lines/module_docstring_blank_lines.py +0 -0
  35. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/module_docstring_blank_lines/module_docstring_blank_lines_100.py +0 -0
  36. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/module_docstring_empty/module_docstring_empty.py +0 -0
  37. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/module_docstring_empty/module_docstring_empty_blank_lines.py +0 -0
  38. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/module_docstring_example_backticks/module_docstring_example_backticks.py +0 -0
  39. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/module_docstring_example_backticks/module_docstring_example_backticks_60.py +0 -0
  40. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/module_docstring_example_tildes/module_docstring_example_tildes.py +0 -0
  41. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/module_docstring_example_tildes/module_docstring_example_tildes_60.py +0 -0
  42. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/module_docstring_ordered_list/module_docstring_ordered_list_100.py +0 -0
  43. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/module_docstring_ordered_list/module_docstring_ordered_list_60.py +0 -0
  44. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/module_docstring_ordered_list/module_docstring_ordered_list_80.py +0 -0
  45. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/module_docstring_ordered_list/module_docstring_ordered_list_too_long.py +0 -0
  46. {docstring_tailor-0.2.0 → docstring_tailor-0.2.1.dev0}/tests/fixtures/readme_examples/readme_examples.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docstring-tailor
3
- Version: 0.2.0
3
+ Version: 0.2.1.dev0
4
4
  Summary: Automatic formatting of Python docstrings according to PEP 257
5
5
  Author-email: Auke Bruinsma <afbruinsma@gmail.com>
6
6
  License: MIT License
@@ -33,22 +33,41 @@ Description-Content-Type: text/markdown
33
33
 
34
34
  # Docstring Tailor 🪡
35
35
 
36
- Automatic formatting of Python docstrings according to PEP 257 and a predefined maximum number of chacacters per line.
36
+ Automatic formatting of Python docstrings according to PEP 257 and a predefined maximum number of characters per line.
37
37
 
38
38
  [![PyPI Version](https://img.shields.io/pypi/v/docstring-tailor?color=lightblue)](https://pypi.org/project/docstring-tailor/)
39
- [![License](https://img.shields.io/pypi/l/docstring-tailor)](https://pypi.org/project/docstring-tailor/)
39
+ [![License](https://img.shields.io/pypi/l/docstring-tailor?color=lightblue)](https://pypi.org/project/docstring-tailor/)
40
40
  [![Wheel](https://img.shields.io/pypi/wheel/docstring-tailor?color=lightblue)](https://pypi.org/project/docstring-tailor/)
41
+ [![Downloads](https://img.shields.io/pypi/dm/docstring-tailor?color=lightblue)](https://pypi.org/project/docstring-tailor/)
42
+
41
43
 
42
44
  ## Table of Contents
43
- 1. [Installation](#Installation)
44
- 2. [Quick start](#quick_start)
45
- 3. [API Overview](#api-overview)
45
+ 1. [Demo](#demo)
46
+ 2. [Installation](#Installation)
47
+ 3. [Quick start](#quick_start)
48
+ 4. [API Overview](#api-overview)
46
49
  - [Command](#command)
47
50
  - [Options](#options)
48
51
  - [Examples](#examples)
49
- 4. [Example docstrings](#example-docstrings)
50
- 5. [Release Notes](#release_notes)
51
- 6. [Roadmap](#roadmap)
52
+ 5. [Example docstrings](#example-docstrings)
53
+ 6. [Release Notes](#release_notes)
54
+ 7. [Roadmap](#roadmap)
55
+
56
+ ## Demo
57
+
58
+ <details>
59
+ <summary><b>Show demo</b></summary>
60
+
61
+ <br>
62
+
63
+ - `docstring-tailor` formats docstrings to fit a given line length, while preserving its structure throughout — For exapmle blank lines, argument indentation, continuation line alignment, and code blocks in the Examples section all remain intact.
64
+ - **Note**: The slider in the [Marimo notebook](https://marimo.io/) is not part of the package. It was created solely to illustrate how the output changes continuously as the line length varies.
65
+
66
+ <br>
67
+
68
+ ![Demo](https://raw.githubusercontent.com/AukeB/docstring-tailor/main/assets/gif_images/docstring_slider.gif)
69
+
70
+ </details>
52
71
 
53
72
  ## Installation
54
73
 
@@ -112,55 +131,56 @@ style = "google"
112
131
  ```bash
113
132
  uv run docstring_tailor [PATHS ...] [OPTIONS]
114
133
  ```
115
-
116
134
  `PATHS` may contain one or more files and/or directories.
117
-
118
135
  Examples:
119
-
120
136
  ```bash
121
137
  uv run docstring_tailor my_file.py
122
138
  uv run docstring_tailor src/
123
139
  uv run docstring_tailor src/ tests/test_file.py
124
140
  ```
125
-
126
141
  If no paths are provided, `docstring_tailor` will attempt to locate and format files inside the `src` directory.
127
142
 
128
- ---
129
-
130
143
  ### Options
131
144
 
132
145
  | <div style="width:140px">Option</div> | <div style="width:50px">Type</div> | <div style="width:80px">Default</div> | Description |
133
146
  |---|---|---|---|
134
- | `--line-length` | `int` | 100 | Maximum number of characters allowed per line after formatting. |
135
- | `--style` | `str` | google | Docstring style to enforce. Currently only the Google docstring style is supported. |
136
- | `--detect-lists` | `bool` | true | Detect unordered and ordered/numbered lists anywhere in a docstring and preserve each list element on its own line during formatting. |
147
+ | `--line-length` | `int` | 100 | Maximum number of characters allowed per line after formatting. |
148
+ | `--style` | `str` | google | Docstring style to enforce. Currently only the Google docstring style is supported. |
149
+ | `--detect-lists` | `bool` | true | Detect unordered and ordered/numbered lists anywhere in a docstring and preserve each list element on its own line during formatting. |
150
+ | `--exclude` | `str` | — | A glob pattern for paths to exclude. Can be passed multiple times. Single-path patterns (e.g. `tests`, `*.pyi`) match by name anywhere in the tree. Relative patterns (e.g. `src/generated/*.py`) match against the path relative to the project root. |
151
+ | `--diff` | flag | — | Print a unified diff of changes to stdout instead of modifying files. No files are written when this flag is set. |
152
+ | `--version`, `-V` | flag | — | Print the installed version and exit. |
153
+ | `--help` | flag | — | Show the help message and exit. |
137
154
 
138
155
  ### Examples
139
156
 
140
157
  `CLI`
141
-
142
158
  ```bash
143
159
  uv run docstring_tailor src/ --line-length 88
144
160
  uv run docstring_tailor my_file.py --style google
145
161
  uv run docstring_tailor --detect-lists
146
162
  uv run docstring_tailor --no-detect-lists
147
- ```
163
+ uv run docstring_tailor src/ --exclude tests --exclude "src/generated/*.py"
164
+ uv run docstring_tailor src/ --diff
165
+ uv run docstring_tailor --version
166
+ uv run docstring_tailor --help
148
167
 
168
+ ```
149
169
  `pyproject.toml`
150
-
151
170
  ```toml
152
171
  [tool.docstring_tailor]
153
172
  line-length = 88
154
173
  style = "google"
155
174
  detect-lists = true
175
+ exclude = ["tests", "src/generated/*.py"]
156
176
  ```
157
177
 
158
178
  `docstring_tailor.toml`
159
-
160
179
  ```toml
161
180
  line-length = 88
162
181
  style = "google"
163
182
  detect-lists = true
183
+ exclude = ["tests", "src/generated/*.py"]
164
184
  ```
165
185
 
166
186
  ## Example docstrings
@@ -315,8 +335,7 @@ def example_generator(n):
315
335
  ```
316
336
  - Similar to `Returns`, `Yields` is also supported.
317
337
 
318
- ```pythonUnordered and numbered lists
319
-
338
+ ```python
320
339
  """Demonstrates a Google-style module docstring containing a Note
321
340
  section.
322
341
 
@@ -378,24 +397,21 @@ Steps:
378
397
 
379
398
  - Personally, I like to use unordered and numbered lists sometimes in a docstring. Similar to what has been described before, **indentation** is used to detect new list elements.
380
399
 
381
-
382
400
  ## Release Notes
383
401
 
384
402
  | <div style="width:70px">Version</div> | <div style="width:100px">Release date</div> | <div style="width:130px">Type</div> | Details |
385
403
  |---|---|---|---|
386
- | `0.1.0` | 2026-05-31 | Initial release | First public release of `docstring-tailor`. Includes <ul><li>Automatic docstring wrapping for module, class and function docstring, for both one line and multi line docstrings, with a configurable `line-length` parameter.</li><li>Paragraph-aware formatting, differentiating between 'Args', 'Examples' or normal text sections.</li> <li> Docstring support for the Google `style` (Numpy, Sphinx, Epydoc not yet supported). </li><li>TOML-based configuration support.</li><li> Test coverage: 52% </ul> |
404
+ | `0.1.0` | 2026-05-31 | Initial release | First public release of `docstring-tailor`. Includes <ul><li>Automatic docstring wrapping for module, class and function docstrings, for both one line and multi line docstrings, with a configurable `line-length` parameter.</li><li>Paragraph-aware formatting, differentiating between 'Args', 'Examples' or normal text sections.</li> <li> Docstring support for the Google `style` (Numpy, Sphinx, Epydoc not yet supported). </li><li>TOML-based configuration support.</li><li> Test coverage: 52% </ul> |
387
405
  | `0.1.1` | 2026-05-31 | Documentation update | Updated the `README.md` file with the 'Installation' and 'Quick Start' section. |
388
406
  | `0.2.0` | 2026-06-07 | Feature update | <ul><li>Implemented the `detect-lists` parameter, adding support for unordered and ordered (numbered) lists in docstrings. When enabled, list structures are detected automatically and each list item is formatted onto its own line.</li><li>Introduced a declarative golden-file test framework for formatter validation. Test cases are now generated from parametrized templates using Cartesian-product expansion, significantly reducing boilerplate and improving scalability for configuration coverage.</li><li>Expanded this `README.md` with the 'API Overview', 'Release Notes', 'Example docstrings' and 'Roadmap' sections.</li><li>Test coverage: 75%</li></ul> |
407
+ | `0.2.1` | 2026-06-10 | Feature update | <ul><li>Added the `-V`/`--version` command to the CLI.</li><li>Added the `--exclude` command to the CLI.</li><li>Added the `--diff` command to the CLI.</li><li>Added the 'Demo' part to to the `README.md`.</ul> |
389
408
 
390
409
  ## Roadmap
391
410
 
392
411
  ### Must have
393
412
 
394
413
  - Support for all major docstrings styles (Google, Numpy, Sphinx, Epydoc).
395
- - Add `diff` functionality that will show you the formatting changes before actually changing the file(s).
396
414
  - Make sure the package can be used as a pre-commit hook.
397
- - Add `exclude` parameters that allows the user to ignore specific files.
398
- - Add `v`/`version` parameter that shows the version of the package.
399
415
 
400
416
  ### Nice to have
401
417
 
@@ -1,21 +1,40 @@
1
1
  # Docstring Tailor 🪡
2
2
 
3
- Automatic formatting of Python docstrings according to PEP 257 and a predefined maximum number of chacacters per line.
3
+ Automatic formatting of Python docstrings according to PEP 257 and a predefined maximum number of characters per line.
4
4
 
5
5
  [![PyPI Version](https://img.shields.io/pypi/v/docstring-tailor?color=lightblue)](https://pypi.org/project/docstring-tailor/)
6
- [![License](https://img.shields.io/pypi/l/docstring-tailor)](https://pypi.org/project/docstring-tailor/)
6
+ [![License](https://img.shields.io/pypi/l/docstring-tailor?color=lightblue)](https://pypi.org/project/docstring-tailor/)
7
7
  [![Wheel](https://img.shields.io/pypi/wheel/docstring-tailor?color=lightblue)](https://pypi.org/project/docstring-tailor/)
8
+ [![Downloads](https://img.shields.io/pypi/dm/docstring-tailor?color=lightblue)](https://pypi.org/project/docstring-tailor/)
9
+
8
10
 
9
11
  ## Table of Contents
10
- 1. [Installation](#Installation)
11
- 2. [Quick start](#quick_start)
12
- 3. [API Overview](#api-overview)
12
+ 1. [Demo](#demo)
13
+ 2. [Installation](#Installation)
14
+ 3. [Quick start](#quick_start)
15
+ 4. [API Overview](#api-overview)
13
16
  - [Command](#command)
14
17
  - [Options](#options)
15
18
  - [Examples](#examples)
16
- 4. [Example docstrings](#example-docstrings)
17
- 5. [Release Notes](#release_notes)
18
- 6. [Roadmap](#roadmap)
19
+ 5. [Example docstrings](#example-docstrings)
20
+ 6. [Release Notes](#release_notes)
21
+ 7. [Roadmap](#roadmap)
22
+
23
+ ## Demo
24
+
25
+ <details>
26
+ <summary><b>Show demo</b></summary>
27
+
28
+ <br>
29
+
30
+ - `docstring-tailor` formats docstrings to fit a given line length, while preserving its structure throughout — For exapmle blank lines, argument indentation, continuation line alignment, and code blocks in the Examples section all remain intact.
31
+ - **Note**: The slider in the [Marimo notebook](https://marimo.io/) is not part of the package. It was created solely to illustrate how the output changes continuously as the line length varies.
32
+
33
+ <br>
34
+
35
+ ![Demo](https://raw.githubusercontent.com/AukeB/docstring-tailor/main/assets/gif_images/docstring_slider.gif)
36
+
37
+ </details>
19
38
 
20
39
  ## Installation
21
40
 
@@ -79,55 +98,56 @@ style = "google"
79
98
  ```bash
80
99
  uv run docstring_tailor [PATHS ...] [OPTIONS]
81
100
  ```
82
-
83
101
  `PATHS` may contain one or more files and/or directories.
84
-
85
102
  Examples:
86
-
87
103
  ```bash
88
104
  uv run docstring_tailor my_file.py
89
105
  uv run docstring_tailor src/
90
106
  uv run docstring_tailor src/ tests/test_file.py
91
107
  ```
92
-
93
108
  If no paths are provided, `docstring_tailor` will attempt to locate and format files inside the `src` directory.
94
109
 
95
- ---
96
-
97
110
  ### Options
98
111
 
99
112
  | <div style="width:140px">Option</div> | <div style="width:50px">Type</div> | <div style="width:80px">Default</div> | Description |
100
113
  |---|---|---|---|
101
- | `--line-length` | `int` | 100 | Maximum number of characters allowed per line after formatting. |
102
- | `--style` | `str` | google | Docstring style to enforce. Currently only the Google docstring style is supported. |
103
- | `--detect-lists` | `bool` | true | Detect unordered and ordered/numbered lists anywhere in a docstring and preserve each list element on its own line during formatting. |
114
+ | `--line-length` | `int` | 100 | Maximum number of characters allowed per line after formatting. |
115
+ | `--style` | `str` | google | Docstring style to enforce. Currently only the Google docstring style is supported. |
116
+ | `--detect-lists` | `bool` | true | Detect unordered and ordered/numbered lists anywhere in a docstring and preserve each list element on its own line during formatting. |
117
+ | `--exclude` | `str` | — | A glob pattern for paths to exclude. Can be passed multiple times. Single-path patterns (e.g. `tests`, `*.pyi`) match by name anywhere in the tree. Relative patterns (e.g. `src/generated/*.py`) match against the path relative to the project root. |
118
+ | `--diff` | flag | — | Print a unified diff of changes to stdout instead of modifying files. No files are written when this flag is set. |
119
+ | `--version`, `-V` | flag | — | Print the installed version and exit. |
120
+ | `--help` | flag | — | Show the help message and exit. |
104
121
 
105
122
  ### Examples
106
123
 
107
124
  `CLI`
108
-
109
125
  ```bash
110
126
  uv run docstring_tailor src/ --line-length 88
111
127
  uv run docstring_tailor my_file.py --style google
112
128
  uv run docstring_tailor --detect-lists
113
129
  uv run docstring_tailor --no-detect-lists
114
- ```
130
+ uv run docstring_tailor src/ --exclude tests --exclude "src/generated/*.py"
131
+ uv run docstring_tailor src/ --diff
132
+ uv run docstring_tailor --version
133
+ uv run docstring_tailor --help
115
134
 
135
+ ```
116
136
  `pyproject.toml`
117
-
118
137
  ```toml
119
138
  [tool.docstring_tailor]
120
139
  line-length = 88
121
140
  style = "google"
122
141
  detect-lists = true
142
+ exclude = ["tests", "src/generated/*.py"]
123
143
  ```
124
144
 
125
145
  `docstring_tailor.toml`
126
-
127
146
  ```toml
128
147
  line-length = 88
129
148
  style = "google"
130
149
  detect-lists = true
150
+ exclude = ["tests", "src/generated/*.py"]
131
151
  ```
132
152
 
133
153
  ## Example docstrings
@@ -282,8 +302,7 @@ def example_generator(n):
282
302
  ```
283
303
  - Similar to `Returns`, `Yields` is also supported.
284
304
 
285
- ```pythonUnordered and numbered lists
286
-
305
+ ```python
287
306
  """Demonstrates a Google-style module docstring containing a Note
288
307
  section.
289
308
 
@@ -345,24 +364,21 @@ Steps:
345
364
 
346
365
  - Personally, I like to use unordered and numbered lists sometimes in a docstring. Similar to what has been described before, **indentation** is used to detect new list elements.
347
366
 
348
-
349
367
  ## Release Notes
350
368
 
351
369
  | <div style="width:70px">Version</div> | <div style="width:100px">Release date</div> | <div style="width:130px">Type</div> | Details |
352
370
  |---|---|---|---|
353
- | `0.1.0` | 2026-05-31 | Initial release | First public release of `docstring-tailor`. Includes <ul><li>Automatic docstring wrapping for module, class and function docstring, for both one line and multi line docstrings, with a configurable `line-length` parameter.</li><li>Paragraph-aware formatting, differentiating between 'Args', 'Examples' or normal text sections.</li> <li> Docstring support for the Google `style` (Numpy, Sphinx, Epydoc not yet supported). </li><li>TOML-based configuration support.</li><li> Test coverage: 52% </ul> |
371
+ | `0.1.0` | 2026-05-31 | Initial release | First public release of `docstring-tailor`. Includes <ul><li>Automatic docstring wrapping for module, class and function docstrings, for both one line and multi line docstrings, with a configurable `line-length` parameter.</li><li>Paragraph-aware formatting, differentiating between 'Args', 'Examples' or normal text sections.</li> <li> Docstring support for the Google `style` (Numpy, Sphinx, Epydoc not yet supported). </li><li>TOML-based configuration support.</li><li> Test coverage: 52% </ul> |
354
372
  | `0.1.1` | 2026-05-31 | Documentation update | Updated the `README.md` file with the 'Installation' and 'Quick Start' section. |
355
373
  | `0.2.0` | 2026-06-07 | Feature update | <ul><li>Implemented the `detect-lists` parameter, adding support for unordered and ordered (numbered) lists in docstrings. When enabled, list structures are detected automatically and each list item is formatted onto its own line.</li><li>Introduced a declarative golden-file test framework for formatter validation. Test cases are now generated from parametrized templates using Cartesian-product expansion, significantly reducing boilerplate and improving scalability for configuration coverage.</li><li>Expanded this `README.md` with the 'API Overview', 'Release Notes', 'Example docstrings' and 'Roadmap' sections.</li><li>Test coverage: 75%</li></ul> |
374
+ | `0.2.1` | 2026-06-10 | Feature update | <ul><li>Added the `-V`/`--version` command to the CLI.</li><li>Added the `--exclude` command to the CLI.</li><li>Added the `--diff` command to the CLI.</li><li>Added the 'Demo' part to to the `README.md`.</ul> |
356
375
 
357
376
  ## Roadmap
358
377
 
359
378
  ### Must have
360
379
 
361
380
  - Support for all major docstrings styles (Google, Numpy, Sphinx, Epydoc).
362
- - Add `diff` functionality that will show you the formatting changes before actually changing the file(s).
363
381
  - Make sure the package can be used as a pre-commit hook.
364
- - Add `exclude` parameters that allows the user to ignore specific files.
365
- - Add `v`/`version` parameter that shows the version of the package.
366
382
 
367
383
  ### Nice to have
368
384
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "docstring-tailor"
3
- version = "0.2.0"
3
+ version = "0.2.1.dev0"
4
4
  description = "Automatic formatting of Python docstrings according to PEP 257"
5
5
  authors = [{ name = "Auke Bruinsma", email = "afbruinsma@gmail.com" }]
6
6
  license = { file = "LICENSE" }
@@ -1,5 +1,6 @@
1
1
  """Module for storing project constants."""
2
2
 
3
+ from collections import namedtuple
3
4
  from pathlib import Path
4
5
 
5
6
  # Repository relative file paths.
@@ -58,3 +59,6 @@ CODE_BLOCK_PREFIXES = (
58
59
  FENCED_CODE_BLOCK_BACKTICKS,
59
60
  FENCED_CODE_BLOCK_TILDES,
60
61
  )
62
+
63
+ # Named tuples
64
+ Section = namedtuple("Section", ["name", "body"])
@@ -1,7 +1,7 @@
1
1
  """Main module"""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Annotated
4
+ from typing import Annotated, Optional
5
5
 
6
6
  import libcst as cst
7
7
  import typer
@@ -18,6 +18,7 @@ from docstring_tailor.cli_config import (
18
18
  )
19
19
  from docstring_tailor.constants import ENCODING
20
20
  from docstring_tailor.docstring_visitor import DocstringVisitor
21
+ from docstring_tailor.utils.utils_cli import show_diff, version_callback
21
22
  from docstring_tailor.utils.utils_file_system import (
22
23
  collect_python_files,
23
24
  load_config,
@@ -33,10 +34,6 @@ def main(
33
34
  list[Path] | None,
34
35
  typer.Argument(help="Files or directories to process. Defaults to 'src/'."),
35
36
  ] = None,
36
- style: Annotated[
37
- DocstringStyle | None,
38
- typer.Option("--style", help="Docstring style to format to."),
39
- ] = None,
40
37
  line_length: Annotated[
41
38
  int | None,
42
39
  typer.Option(
@@ -46,6 +43,10 @@ def main(
46
43
  max=LINE_LENGTH_MAX,
47
44
  ),
48
45
  ] = None,
46
+ style: Annotated[
47
+ DocstringStyle | None,
48
+ typer.Option("--style", help="Docstring style to format to."),
49
+ ] = None,
49
50
  detect_lists: Annotated[
50
51
  bool | None,
51
52
  typer.Option(
@@ -53,6 +54,35 @@ def main(
53
54
  help="Detect and preserve list formatting.",
54
55
  ),
55
56
  ] = None,
57
+ exclude: Annotated[
58
+ list[str] | None,
59
+ typer.Option(
60
+ "--exclude",
61
+ help=(
62
+ "A glob pattern for paths to exclude. Can be passed multiple times. "
63
+ "Single-path patterns (e.g. 'tests', '*.pyi') match by name anywhere "
64
+ "in the tree. Relative patterns (e.g. 'src/generated/*.py') match "
65
+ "against the path relative to the project root."
66
+ ),
67
+ ),
68
+ ] = None,
69
+ diff: Annotated[
70
+ bool,
71
+ typer.Option(
72
+ "--diff",
73
+ help="Show a diff of changes without modifying any files.",
74
+ ),
75
+ ] = False,
76
+ version: Annotated[
77
+ Optional[bool],
78
+ typer.Option(
79
+ "-V",
80
+ "--version",
81
+ callback=version_callback,
82
+ is_eager=True,
83
+ help="Show the version and exit.",
84
+ ),
85
+ ] = None,
56
86
  ) -> None:
57
87
  """Formats Python docstrings in the given files or directories to the specified style.
58
88
 
@@ -61,23 +91,28 @@ def main(
61
91
 
62
92
  Args:
63
93
  paths (list[Path] | None): Files or directories to process. Defaults to 'src/'.
64
- style (DocstringStyle | None): The docstring style to format to.
65
94
  line_length (int | None): The maximum line length to wrap docstrings to.
95
+ style (DocstringStyle | None): The docstring style to format to.
96
+ detect_lists (bool | None): Whether to detect and preserve list formatting.
97
+ diff (bool): If True, print a unified diff to stdout instead of writing files.
98
+ exclude (list[str] | None): Glob patterns for paths to exclude.
99
+ version (bool | None): If passed, print the version and exit.
66
100
  """
67
101
  # Resolve configuration with priority: CLI argument > config file > built-in default.
68
102
  file_config = load_config()
69
103
  resolved_paths = paths or [Path(p) for p in DEFAULT_PATHS]
70
- resolved_style = style or DocstringStyle(
71
- file_config.get("style", DEFAULT_STYLE.value)
72
- )
73
104
  resolved_line_length = line_length or file_config.get(
74
105
  "line-length", LINE_LENGTH_DEFAULT
75
106
  )
107
+ resolved_style = style or DocstringStyle(
108
+ file_config.get("style", DEFAULT_STYLE.value)
109
+ )
76
110
  resolved_detect_lists = (
77
111
  detect_lists
78
112
  if detect_lists is not None
79
113
  else file_config.get("detect-lists", DETECT_LISTS_DEFAULT)
80
114
  )
115
+ resolved_exclude = exclude or file_config.get("exclude", [])
81
116
 
82
117
  if resolved_style not in SUPPORTED_STYLES:
83
118
  typer.echo(
@@ -87,7 +122,10 @@ def main(
87
122
  raise typer.Exit(code=1)
88
123
 
89
124
  validate_paths(paths=resolved_paths)
90
- python_files = collect_python_files(paths=resolved_paths)
125
+ python_files = collect_python_files(
126
+ paths=resolved_paths,
127
+ exclude_patterns=resolved_exclude,
128
+ )
91
129
 
92
130
  for file_path in python_files:
93
131
  input_data = file_path.read_text(encoding=ENCODING)
@@ -97,25 +135,14 @@ def main(
97
135
  line_length=resolved_line_length, detect_lists=resolved_detect_lists
98
136
  )
99
137
  )
100
- file_path.write_text(modified_tree.code, encoding=ENCODING)
101
138
 
139
+ modified_code = modified_tree.code
102
140
 
103
- if __name__ == "__main__":
104
- app()
105
-
106
-
107
- """TODO:
141
+ if diff:
142
+ show_diff(original=input_data, modified=modified_code, path=file_path)
143
+ else:
144
+ file_path.write_text(modified_code, encoding=ENCODING)
108
145
 
109
- - Fix issue with (un)ordered lists if list element span multiple lines.
110
146
 
111
- - Currently, this code has been written specifically for the 'Google' docstring format. Fine for
112
- now, but the end state goal is to have the functionality that the user can specify the style in the
113
- pyproject.toml and that everything formats correctly to that style. The reading from pyproject.toml
114
- is already there, the biggest effort is in reformatting docstring_section_formatter a bit to make it
115
- work for all styles.
116
-
117
- - Check all the parameters in the docstringformatter package to see which ones I also want.
118
-
119
- - Implement feature that you can display the diff in terminal, instead of immediately formatting and
120
- overwriting the .py files.
121
- """
147
+ if __name__ == "__main__":
148
+ app()
@@ -2,7 +2,6 @@
2
2
 
3
3
  import re
4
4
  import textwrap
5
- from collections import namedtuple
6
5
 
7
6
  from docstring_tailor.constants import (
8
7
  CODE_BLOCK_PREFIXES,
@@ -11,14 +10,10 @@ from docstring_tailor.constants import (
11
10
  GOOGLE_ITEM_SECTIONS,
12
11
  GOOGLE_PLAIN_SECTIONS,
13
12
  GOOGLE_SECTION_HEADERS,
13
+ Section,
14
14
  )
15
- from docstring_tailor.utils.utils_formatting import (
16
- format_list,
17
- format_paragraph,
18
- is_list,
19
- )
20
-
21
- Section = namedtuple("Section", ["name", "body"])
15
+ from docstring_tailor.utils.utils_formatting import format_list, format_paragraph
16
+ from docstring_tailor.utils.utils_list_detection import is_list
22
17
 
23
18
 
24
19
  class MultiLineDocstringFormatter:
@@ -0,0 +1,55 @@
1
+ """Utility module containing helper functions for the CLI."""
2
+
3
+ import difflib
4
+ from importlib import metadata
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+
10
+ def version_callback(value: bool) -> None:
11
+ """Print the package version and exit.
12
+
13
+ Args:
14
+ value (bool): Whether the flag was passed.
15
+
16
+ Raises:
17
+ typer.Exit: Always raised after printing, to halt execution.
18
+ """
19
+ if value:
20
+ version = metadata.version("docstring-tailor")
21
+ typer.echo(version)
22
+ raise typer.Exit()
23
+
24
+
25
+ def show_diff(original: str, modified: str, path: Path) -> None:
26
+ """Prints a unified diff between the original and modified source to stdout.
27
+
28
+ Skips output entirely if the two sources are identical. Each line of the diff is coloured:
29
+ additions in green, removals in red, and header lines in bold, falling back to plain output on
30
+ terminals that don't support ANSI codes.
31
+
32
+ Args:
33
+ original (str): The source text before formatting.
34
+ modified (str): The source text after formatting.
35
+ path (Path): The file path, used as the diff header label.
36
+ """
37
+ if original == modified:
38
+ return
39
+
40
+ diff_lines = difflib.unified_diff(
41
+ original.splitlines(keepends=True),
42
+ modified.splitlines(keepends=True),
43
+ fromfile=f"{path} (original)",
44
+ tofile=f"{path} (formatted)",
45
+ )
46
+
47
+ for line in diff_lines:
48
+ if line.startswith("+++") or line.startswith("---"):
49
+ typer.echo(typer.style(line, bold=True), nl=False)
50
+ elif line.startswith("+"):
51
+ typer.echo(typer.style(line, fg=typer.colors.GREEN), nl=False)
52
+ elif line.startswith("-"):
53
+ typer.echo(typer.style(line, fg=typer.colors.RED), nl=False)
54
+ else:
55
+ typer.echo(line, nl=False)
@@ -0,0 +1,117 @@
1
+ """Module containing various utility functions related to interacting with local file system."""
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+
8
+ def load_config() -> dict:
9
+ """Loads configuration from docstring_tailor.toml or pyproject.toml.
10
+
11
+ Walks up from the current directory. docstring_tailor.toml takes priority over pyproject.toml if
12
+ both exist at the same level. Stops at the first file found containing docstring_tailor
13
+ configuration.
14
+
15
+ Returns:
16
+ config (dict): Configuration settings, or an empty dict if none found.
17
+ """
18
+ import tomllib
19
+
20
+ for directory in [Path.cwd(), *Path.cwd().parents]:
21
+ tailor_config = directory / "docstring_tailor.toml"
22
+ if tailor_config.exists():
23
+ with open(tailor_config, "rb") as file:
24
+ return tomllib.load(file)
25
+
26
+ pyproject = directory / "pyproject.toml"
27
+ if pyproject.exists():
28
+ with open(pyproject, "rb") as file:
29
+ data = tomllib.load(file)
30
+ tool_config = data.get("tool", {}).get("docstring_tailor", {})
31
+ if tool_config:
32
+ return tool_config
33
+
34
+ return {}
35
+
36
+
37
+ def _is_excluded(path: Path, exclude_patterns: list[str], project_root: Path) -> bool:
38
+ """Checks whether a path matches any of the provided exclusion patterns.
39
+
40
+ Supports two pattern types, mirroring Ruff's exclude behaviour:
41
+ - Single-path patterns (e.g. '.mypy_cache', 'foo.py', 'foo_*.py') are matched against every
42
+ component of the path, so they exclude by name anywhere in the tree.
43
+ - Relative patterns containing a separator (e.g. 'directory/foo.py', 'directory/*.py') are
44
+ matched against the path relative to the project root, so they only exclude at that specific
45
+ location.
46
+
47
+ Args:
48
+ path (Path): The absolute path to test.
49
+ exclude_patterns (list[str]): Glob patterns provided via --exclude.
50
+ project_root (Path): The directory against which relative patterns are resolved (typically
51
+ cwd or the directory containing pyproject.toml).
52
+
53
+ Returns:
54
+ excluded (bool): True if the path matches at least one pattern.
55
+ """
56
+ for pattern in exclude_patterns:
57
+ is_relative_pattern = "/" in pattern or "\\" in pattern
58
+ if is_relative_pattern:
59
+ try:
60
+ relative_path = path.relative_to(project_root)
61
+ if relative_path.match(pattern):
62
+ return True
63
+ except ValueError:
64
+ pass
65
+ else:
66
+ for part in path.parts:
67
+ if Path(part).match(pattern):
68
+ return True
69
+
70
+ return False
71
+
72
+
73
+ def collect_python_files(
74
+ paths: list[Path],
75
+ exclude_patterns: list[str] | None = None,
76
+ ) -> list[Path]:
77
+ """Collects all Python files from a list of file and/or directory paths.
78
+
79
+ Directories are searched recursively. Files are included directly. Any path matching one of the
80
+ provided exclusion patterns is silently skipped.
81
+
82
+ Args:
83
+ paths (list[Path]): A list of file and/or directory paths to search.
84
+ exclude_patterns (list[str] | None): Glob patterns for paths to exclude.
85
+
86
+ Returns:
87
+ python_files (list[Path]): A flat list of all collected .py file paths.
88
+ """
89
+ resolved_patterns = exclude_patterns or []
90
+ project_root = Path.cwd()
91
+ python_files: list[Path] = []
92
+
93
+ for path in paths:
94
+ if path.is_dir():
95
+ for candidate in path.rglob("*.py"):
96
+ if not _is_excluded(candidate, resolved_patterns, project_root):
97
+ python_files.append(candidate)
98
+ else:
99
+ if not _is_excluded(path, resolved_patterns, project_root):
100
+ python_files.append(path)
101
+
102
+ return python_files
103
+
104
+
105
+ def validate_paths(paths: list[Path]) -> None:
106
+ """Validates that all provided paths exist on the filesystem.
107
+
108
+ Args:
109
+ paths (list[Path]): A list of file and/or directory paths to validate.
110
+
111
+ Raises:
112
+ typer.Exit: If any path does not exist.
113
+ """
114
+ for path in paths:
115
+ if not path.exists():
116
+ typer.echo(f"Error: path '{path}' does not exist.")
117
+ raise typer.Exit(code=1)
@@ -33,91 +33,6 @@ def format_paragraph(
33
33
  return formatted
34
34
 
35
35
 
36
- def _is_unordered_list(lines: list[str]) -> bool:
37
- """Returns True if at least two consecutive list items starting with an unordered marker are
38
- found, ignoring continuation lines from wrapped items.
39
-
40
- Args:
41
- lines (list[str]): The lines to check.
42
-
43
- Returns:
44
- result (bool): True if an unordered list is detected, False otherwise.
45
- """
46
- non_empty_lines = [line for line in lines if line.strip()]
47
- if not non_empty_lines:
48
- return False
49
-
50
- base_indent = min(len(line) - len(line.lstrip()) for line in non_empty_lines)
51
- pattern = re.compile(r"^\s*[-*+]\s+")
52
- count = 0
53
-
54
- for line in lines:
55
- if not line.strip():
56
- continue
57
-
58
- if len(line) - len(line.lstrip()) == base_indent:
59
- if pattern.match(line):
60
- count += 1
61
- if count >= 2:
62
- return True
63
- else:
64
- count = 0
65
-
66
- return False
67
-
68
-
69
- def _is_ordered_list(lines: list[str]) -> bool:
70
- """Returns True if at least two consecutive sequentially numbered list items are found, ignoring
71
- continuation lines from wrapped items.
72
-
73
- Args:
74
- lines (list[str]): The lines to check.
75
-
76
- Returns:
77
- result (bool): True if an ordered list is detected, False otherwise.
78
- """
79
- non_empty_lines = [line for line in lines if line.strip()]
80
- if not non_empty_lines:
81
- return False
82
-
83
- base_indent = min(len(line) - len(line.lstrip()) for line in non_empty_lines)
84
- pattern = re.compile(r"^\s*(\d+)[.)]\s+")
85
- expected = 1
86
- count = 0
87
-
88
- for line in lines:
89
- if not line.strip():
90
- continue
91
-
92
- if len(line) - len(line.lstrip()) == base_indent:
93
- match = pattern.match(line)
94
- if match and int(match.group(1)) == expected:
95
- count += 1
96
- expected += 1
97
- if count >= 2:
98
- return True
99
- else:
100
- expected = 1
101
- count = 0
102
-
103
- return False
104
-
105
-
106
- def is_list(text: str) -> bool:
107
- """Returns True if the text block contains an unordered or ordered list.
108
-
109
- Args:
110
- text (str): The text block to check.
111
-
112
- Returns:
113
- result (bool): True if a list is detected, False otherwise.
114
- """
115
- lines = text.split("\n")
116
- result = _is_unordered_list(lines=lines) or _is_ordered_list(lines=lines)
117
-
118
- return result
119
-
120
-
121
36
  def format_list(text: str, wrap_width: int, line_separator: str) -> str:
122
37
  """Formats a list block, preserving each item on its own line.
123
38
 
@@ -0,0 +1,88 @@
1
+ """Utility module with functions for list detection in docstrings."""
2
+
3
+ import re
4
+
5
+
6
+ def _is_unordered_list(lines: list[str]) -> bool:
7
+ """Returns True if at least two consecutive list items starting with an unordered marker are
8
+ found, ignoring continuation lines from wrapped items.
9
+
10
+ Args:
11
+ lines (list[str]): The lines to check.
12
+
13
+ Returns:
14
+ result (bool): True if an unordered list is detected, False otherwise.
15
+ """
16
+ non_empty_lines = [line for line in lines if line.strip()]
17
+ if not non_empty_lines:
18
+ return False
19
+
20
+ base_indent = min(len(line) - len(line.lstrip()) for line in non_empty_lines)
21
+ pattern = re.compile(r"^\s*[-*+]\s+")
22
+ count = 0
23
+
24
+ for line in lines:
25
+ if not line.strip():
26
+ continue
27
+
28
+ if len(line) - len(line.lstrip()) == base_indent:
29
+ if pattern.match(line):
30
+ count += 1
31
+ if count >= 2:
32
+ return True
33
+ else:
34
+ count = 0
35
+
36
+ return False
37
+
38
+
39
+ def _is_ordered_list(lines: list[str]) -> bool:
40
+ """Returns True if at least two consecutive sequentially numbered list items are found, ignoring
41
+ continuation lines from wrapped items.
42
+
43
+ Args:
44
+ lines (list[str]): The lines to check.
45
+
46
+ Returns:
47
+ result (bool): True if an ordered list is detected, False otherwise.
48
+ """
49
+ non_empty_lines = [line for line in lines if line.strip()]
50
+ if not non_empty_lines:
51
+ return False
52
+
53
+ base_indent = min(len(line) - len(line.lstrip()) for line in non_empty_lines)
54
+ pattern = re.compile(r"^\s*(\d+)[.)]\s+")
55
+ expected = 1
56
+ count = 0
57
+
58
+ for line in lines:
59
+ if not line.strip():
60
+ continue
61
+
62
+ if len(line) - len(line.lstrip()) == base_indent:
63
+ match = pattern.match(line)
64
+ if match and int(match.group(1)) == expected:
65
+ count += 1
66
+ expected += 1
67
+ if count >= 2:
68
+ return True
69
+ else:
70
+ expected = 1
71
+ count = 0
72
+
73
+ return False
74
+
75
+
76
+ def is_list(text: str) -> bool:
77
+ """Returns True if the text block contains an unordered or ordered list.
78
+
79
+ Args:
80
+ text (str): The text block to check.
81
+
82
+ Returns:
83
+ result (bool): True if a list is detected, False otherwise.
84
+ """
85
+ lines = text.split("\n")
86
+ result = _is_unordered_list(lines=lines) or _is_ordered_list(lines=lines)
87
+
88
+ return result
@@ -4,9 +4,9 @@ import libcst as cst
4
4
  import pytest
5
5
 
6
6
  from docstring_tailor.docstring_visitor import DocstringVisitor
7
+ from docstring_tailor.utils.utils_testing import generate_case_ids, read_fixture
7
8
  from tests.cases.config_model import Case
8
9
  from tests.cases.formatting_cases import CASES
9
- from tests.utils_test import generate_case_ids, read_fixture
10
10
 
11
11
 
12
12
  @pytest.mark.parametrize("case", CASES, ids=generate_case_ids)
@@ -119,7 +119,7 @@ toml = [
119
119
 
120
120
  [[package]]
121
121
  name = "docstring-tailor"
122
- version = "0.2.0"
122
+ version = "0.2.1.dev0"
123
123
  source = { editable = "." }
124
124
  dependencies = [
125
125
  { name = "libcst" },
@@ -1,70 +0,0 @@
1
- """Module containing various utility functions."""
2
-
3
- from pathlib import Path
4
-
5
- import typer
6
-
7
-
8
- def load_config() -> dict:
9
- """Loads configuration from docstring_tailor.toml or pyproject.toml.
10
-
11
- Walks up from the current directory. docstring_tailor.toml takes priority over pyproject.toml if
12
- both exist at the same level. Stops at the first file found containing docstring_tailor
13
- configuration.
14
-
15
- Returns:
16
- config (dict): Configuration settings, or an empty dict if none found.
17
- """
18
- import tomllib
19
-
20
- for directory in [Path.cwd(), *Path.cwd().parents]:
21
- tailor_config = directory / "docstring_tailor.toml"
22
- if tailor_config.exists():
23
- with open(tailor_config, "rb") as file:
24
- return tomllib.load(file)
25
-
26
- pyproject = directory / "pyproject.toml"
27
- if pyproject.exists():
28
- with open(pyproject, "rb") as file:
29
- data = tomllib.load(file)
30
- tool_config = data.get("tool", {}).get("docstring_tailor", {})
31
- if tool_config:
32
- return tool_config
33
-
34
- return {}
35
-
36
-
37
- def collect_python_files(paths: list[Path]) -> list[Path]:
38
- """Collects all Python files from a list of file and/or directory paths.
39
-
40
- Directories are searched recursively. Files are included directly.
41
-
42
- Args:
43
- paths (list[Path]): A list of file and/or directory paths to search.
44
-
45
- Returns:
46
- python_files (list[Path]): A flat list of all collected .py file paths.
47
- """
48
- python_files: list[Path] = []
49
- for path in paths:
50
- if path.is_dir():
51
- python_files.extend(path.rglob("*.py"))
52
- else:
53
- python_files.append(path)
54
-
55
- return python_files
56
-
57
-
58
- def validate_paths(paths: list[Path]) -> None:
59
- """Validates that all provided paths exist on the filesystem.
60
-
61
- Args:
62
- paths (list[Path]): A list of file and/or directory paths to validate.
63
-
64
- Raises:
65
- typer.Exit: If any path does not exist.
66
- """
67
- for path in paths:
68
- if not path.exists():
69
- typer.echo(f"Error: path '{path}' does not exist.")
70
- raise typer.Exit(code=1)