format-docstring 0.1.7__tar.gz → 0.1.9__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 (106) hide show
  1. {format_docstring-0.1.7 → format_docstring-0.1.9}/.pre-commit-config.yaml +24 -0
  2. format_docstring-0.1.9/AGENTS.md +93 -0
  3. {format_docstring-0.1.7 → format_docstring-0.1.9}/CHANGELOG.md +35 -0
  4. {format_docstring-0.1.7 → format_docstring-0.1.9}/PKG-INFO +22 -2
  5. {format_docstring-0.1.7 → format_docstring-0.1.9}/README.md +21 -0
  6. {format_docstring-0.1.7 → format_docstring-0.1.9}/format_docstring/base_fixer.py +26 -2
  7. {format_docstring-0.1.7 → format_docstring-0.1.9}/format_docstring/docstring_rewriter.py +71 -29
  8. {format_docstring-0.1.7 → format_docstring-0.1.9}/format_docstring/line_wrap_numpy.py +98 -38
  9. {format_docstring-0.1.7 → format_docstring-0.1.9}/format_docstring/line_wrap_utils.py +62 -32
  10. {format_docstring-0.1.7 → format_docstring-0.1.9}/format_docstring/main_jupyter.py +21 -6
  11. {format_docstring-0.1.7 → format_docstring-0.1.9}/format_docstring/main_py.py +16 -1
  12. {format_docstring-0.1.7 → format_docstring-0.1.9}/format_docstring.egg-info/PKG-INFO +22 -2
  13. {format_docstring-0.1.7 → format_docstring-0.1.9}/format_docstring.egg-info/SOURCES.txt +7 -1
  14. {format_docstring-0.1.7 → format_docstring-0.1.9}/format_docstring.egg-info/requires.txt +0 -1
  15. {format_docstring-0.1.7 → format_docstring-0.1.9}/muff.toml +1 -1
  16. {format_docstring-0.1.7 → format_docstring-0.1.9}/pyproject.toml +1 -2
  17. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/helpers.py +2 -1
  18. format_docstring-0.1.9/tests/test_base_fixer.py +47 -0
  19. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_config.py +2 -0
  20. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/fix_rst_backticks.txt +11 -3
  21. format_docstring-0.1.9/tests/test_data/end_to_end/numpy/no_format_docstring_comment.txt +54 -0
  22. format_docstring-0.1.9/tests/test_data/end_to_end/numpy/section_headings_with_colons.txt +68 -0
  23. format_docstring-0.1.9/tests/test_data/jupyter/before.ipynb +28 -0
  24. format_docstring-0.1.9/tests/test_data/jupyter/verbose_before.ipynb +28 -0
  25. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/fix_rst_backticks.txt +10 -2
  26. format_docstring-0.1.9/tests/test_data/line_wrap/numpy/section_headings_with_colons.txt +60 -0
  27. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_docstring_rewriter.py +5 -0
  28. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_line_wrap_numpy.py +23 -1
  29. format_docstring-0.1.9/tests/test_main_jupyter.py +90 -0
  30. format_docstring-0.1.9/tests/test_main_py.py +88 -0
  31. format_docstring-0.1.7/CLAUDE.md +0 -151
  32. format_docstring-0.1.7/tests/test_main_jupyter.py +0 -45
  33. format_docstring-0.1.7/tests/test_main_py.py +0 -42
  34. {format_docstring-0.1.7 → format_docstring-0.1.9}/.github/workflows/python-package.yml +0 -0
  35. {format_docstring-0.1.7 → format_docstring-0.1.9}/.github/workflows/python-publish.yml +0 -0
  36. {format_docstring-0.1.7 → format_docstring-0.1.9}/.gitignore +0 -0
  37. {format_docstring-0.1.7 → format_docstring-0.1.9}/.pre-commit-hooks.yaml +0 -0
  38. {format_docstring-0.1.7 → format_docstring-0.1.9}/LICENSE +0 -0
  39. {format_docstring-0.1.7 → format_docstring-0.1.9}/format_docstring/__init__.py +0 -0
  40. {format_docstring-0.1.7 → format_docstring-0.1.9}/format_docstring/config.py +0 -0
  41. {format_docstring-0.1.7 → format_docstring-0.1.9}/format_docstring/line_wrap_google.py +0 -0
  42. {format_docstring-0.1.7 → format_docstring-0.1.9}/format_docstring.egg-info/dependency_links.txt +0 -0
  43. {format_docstring-0.1.7 → format_docstring-0.1.9}/format_docstring.egg-info/entry_points.txt +0 -0
  44. {format_docstring-0.1.7 → format_docstring-0.1.9}/format_docstring.egg-info/top_level.txt +0 -0
  45. {format_docstring-0.1.7 → format_docstring-0.1.9}/requirements.dev +0 -0
  46. {format_docstring-0.1.7 → format_docstring-0.1.9}/setup.cfg +0 -0
  47. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/__init__.py +0 -0
  48. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/README.md +0 -0
  49. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/colon_spacing_fix.txt +0 -0
  50. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/contents_that_are_not_wrapped.txt +0 -0
  51. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/default_value_standardization.txt +0 -0
  52. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/empty_lines_are_respected.txt +0 -0
  53. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/examples_section.txt +0 -0
  54. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/existing_linebreaks_should_not_be_respected.txt +0 -0
  55. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/four_level_nested_classes.txt +0 -0
  56. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/indent_four_levels_16_spaces_width_10.txt +0 -0
  57. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/indent_misaligned_all.txt +0 -0
  58. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/indent_two_levels_8_spaces.txt +0 -0
  59. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/line_length_2.txt +0 -0
  60. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/mismatched_underlines.txt +0 -0
  61. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/mismatched_underlines_one_dash.txt +0 -0
  62. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/mismatched_underlines_two_dashes.txt +0 -0
  63. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/module_level_docstring.txt +0 -0
  64. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/new_lines_before_and_after.txt +0 -0
  65. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/param_signature_without_type.txt +0 -0
  66. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/parameters_returns_raises_wrapping.txt +0 -0
  67. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/returns_signature_and_description.txt +0 -0
  68. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/section_title_fixed.txt +0 -0
  69. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/sections_notes_examples.txt +0 -0
  70. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/signature_line_is_not_wrapped.txt +0 -0
  71. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/single_line_docstring.txt +0 -0
  72. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/texts_are_rewrapped.txt +0 -0
  73. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/end_to_end/numpy/very_long_unbreakable_word.txt +0 -0
  74. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/integration_test/numpy/after.ipynb +0 -0
  75. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/integration_test/numpy/after.py +0 -0
  76. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/integration_test/numpy/after_50.ipynb +0 -0
  77. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/integration_test/numpy/after_50.py +0 -0
  78. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/integration_test/numpy/before.ipynb +0 -0
  79. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/integration_test/numpy/before.py +0 -0
  80. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/README.md +0 -0
  81. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/colon_spacing_fix.txt +0 -0
  82. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/contents_that_are_not_wrapped.txt +0 -0
  83. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/default_value_standardization.txt +0 -0
  84. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/empty_lines_are_respected.txt +0 -0
  85. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/examples_section.txt +0 -0
  86. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/existing_linebreaks_should_not_be_respected.txt +0 -0
  87. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/indent_four_levels_16_spaces_width_10.txt +0 -0
  88. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/indent_two_levels_8_spaces.txt +0 -0
  89. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/line_length_2.txt +0 -0
  90. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/mismatched_underlines.txt +0 -0
  91. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/mismatched_underlines_one_dash.txt +0 -0
  92. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/mismatched_underlines_two_dashes.txt +0 -0
  93. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/module_level_docstring.txt +0 -0
  94. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/param_signature_without_type.txt +0 -0
  95. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/parameters_returns_raises_wrapping.txt +0 -0
  96. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/returns_signature_and_description.txt +0 -0
  97. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/section_title_fixed.txt +0 -0
  98. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/sections_notes_examples.txt +0 -0
  99. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/signature_line_is_not_wrapped.txt +0 -0
  100. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/texts_are_rewrapped.txt +0 -0
  101. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/line_wrap/numpy/very_long_unbreakable_word.txt +0 -0
  102. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_data/playground.py +0 -0
  103. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_line_wrap_google.py +0 -0
  104. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_line_wrap_utils.py +0 -0
  105. {format_docstring-0.1.7 → format_docstring-0.1.9}/tests/test_playground.py +0 -0
  106. {format_docstring-0.1.7 → format_docstring-0.1.9}/tox.ini +0 -0
@@ -40,3 +40,27 @@ repos:
40
40
  rev: 0.0.11
41
41
  hooks:
42
42
  - id: markdown-toc-creator
43
+ - repo: local
44
+ hooks:
45
+ - id: format-docstring
46
+ name: Format self (python docstrings)
47
+ entry: python
48
+ args: [-m, format_docstring.main_py, --verbose, diff]
49
+ language: python
50
+ types: [python]
51
+ exclude: ^tests/test_data/
52
+ additional_dependencies:
53
+ - click>=8.0
54
+ - jupyter-notebook-parser>=0.1.4
55
+ - tomli>=1.1.0
56
+ - id: format-docstring-jupyter
57
+ name: Format self (docstrings in Jupyter notebooks)
58
+ entry: python
59
+ args: [-m, format_docstring.main_jupyter, --verbose, diff]
60
+ language: python
61
+ files: ^.*\.ipynb$
62
+ exclude: ^tests/test_data/
63
+ additional_dependencies:
64
+ - click>=8.0
65
+ - jupyter-notebook-parser>=0.1.4
66
+ - tomli>=1.1.0
@@ -0,0 +1,93 @@
1
+ # AGENTS.md
2
+
3
+ This guide briefs coding agents working on `format-docstring`. Use it to get
4
+ oriented before making changes.
5
+
6
+ ## Quick Snapshot
7
+
8
+ - Formats NumPy-style docstrings (with experimental Google support) in `.py`
9
+ files and Jupyter notebooks while preserving surrounding code.
10
+ - Distributed as a CLI (`format-docstring`, `format-docstring-jupyter`) with
11
+ minimum Python 3.10, packaged via `setuptools`.
12
+ - Core dependencies: `click`, `docstring_parser_fork`,
13
+ `jupyter-notebook-parser`, and `tomli/tomllib` for configuration loading.
14
+ - Version is sourced dynamically in `format_docstring/__init__.py`.
15
+
16
+ ## Repository Layout
17
+
18
+ - `format_docstring/main_py.py` – Click CLI for Python files; validates input
19
+ and delegates to `PythonFileFixer`.
20
+ - `format_docstring/main_jupyter.py` – CLI for `.ipynb` files built on
21
+ `JupyterNotebookParser`/`JupyterNotebookRewriter`.
22
+ - `format_docstring/base_fixer.py` – Shared directory traversal, exclusion
23
+ regex handling, and `fix_one_directory_or_one_file` orchestration.
24
+ - `format_docstring/docstring_rewriter.py` – AST-based docstring extraction and
25
+ replacement that leaves non-docstring text untouched.
26
+ - `format_docstring/line_wrap_numpy.py` / `line_wrap_google.py` –
27
+ Style-specific wrapping helpers; NumPy path is the production code path,
28
+ Google is partial.
29
+ - `format_docstring/line_wrap_utils.py` – Shared utilities for wrapping (indent
30
+ management, bullet handling, preserving literal blocks, etc.).
31
+ - `format_docstring/config.py` – `pyproject.toml` discovery, parsing, and Click
32
+ default injection.
33
+ - `tests/` – Pytest suite with fixture-driven cases in `tests/test_data/`; see
34
+ `tests/helpers.py` for fixture loading helpers.
35
+
36
+ ## Implementation Notes
37
+
38
+ - `docstring_rewriter.fix_src` parses with `ast.parse`, collects docstring
39
+ literals, and rewrites source slices using absolute offsets from
40
+ `calc_line_starts`; this avoids `ast.unparse` and keeps comments/spacing.
41
+ - Wrapping honors NumPy section heuristics, rST constructs, code fences,
42
+ `Examples` prompts, and literal blocks introduced by `::`.
43
+ - CLI exposes `--docstring-style`, but the Python entry-point currently raises
44
+ if a non-NumPy style is requested; Jupyter flow passes style through
45
+ unchanged.
46
+ - `BaseFixer` subclasses return `1` when any file changed so callers can
47
+ surface non-zero exit codes.
48
+ - Notebook fixer round-trips JSON via `json.dump(..., indent=1)` and rewrites
49
+ cells only when content changes, preserving magics with `reconstruct_source`.
50
+
51
+ ## Configuration
52
+
53
+ - User-facing configuration lives under `[tool.format_docstring]` inside
54
+ `pyproject.toml` and supports `line_length`, `docstring_style`, `exclude`,
55
+ `fix_rst_backticks`, and `verbose` (`default` or `diff` to print unified
56
+ diffs on rewrites).
57
+ - `config.inject_config_from_file` auto-discovers the nearest `pyproject.toml`
58
+ (walking up from targets) and merges values into Click’s `default_map`.
59
+ - Default exclude pattern is `\.git|\.tox|\.pytest_cache`; tests tweak it as
60
+ needed.
61
+
62
+ ## Development Workflow
63
+
64
+ - Install: `pip install -e .` for the project,
65
+ `pip install -r requirements.dev` for tooling.
66
+ - Tests: `pytest --tb=long`, or target modules such as
67
+ `pytest tests/test_docstring_rewriter.py`.
68
+ - Lint/format: `muff check --fix --config=muff.toml format_docstring tests`,
69
+ `muff format --diff --config=muff.toml format_docstring tests`.
70
+ - Type checking: `mypy format_docstring/`.
71
+ - Tox: `tox` for the full matrix (py310–py313, mypy, lint), or focused runs
72
+ like `tox -e py311`, `tox -e mypy`, `tox -e muff-format`.
73
+ - CLI smoke tests: `format-docstring --help`,
74
+ `format-docstring-jupyter --help`.
75
+ - Pre-commit: `pre-commit run -a`.
76
+
77
+ ## Testing Notes
78
+
79
+ - Fixture files under `tests/test_data/line_wrap` and
80
+ `tests/test_data/end_to_end` use `LINE_LENGTH: <int>` headers followed by
81
+ `BEFORE`/`AFTER` sections split by `**********`.
82
+ - `tests/test_playground.py` focuses on regression snippets;
83
+ `tests/test_config.py` exercises config discovery and CLI overrides.
84
+ - When modifying wrapping rules, update both the helper (`line_wrap_utils.py`)
85
+ and the corresponding expectation files in `tests/test_data/`.
86
+
87
+ ## Style Guidance
88
+
89
+ - Formatting rules mirror `muff.toml` (line length 79, single quotes, NumPy
90
+ docstring convention). Respect these when adding code.
91
+ - Keep docstring style tests conservative: avoid mutating non-docstring
92
+ content, and add regression cases whenever handling around literal sections
93
+ or tables changes.
@@ -6,6 +6,39 @@ The format is based on
6
6
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
7
7
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.1.9] - 2025-10-16
10
+
11
+ - Added
12
+ - A "`# no-format-docstring`" directive to ignore certain docstring
13
+ - Verbose diff output via `--verbose diff` and
14
+ `[tool.format_docstring] verbose`
15
+ - Normalize NumPy section headings that include trailing colons (e.g.,
16
+ `Parameters:`); also, fix Google-style "Arg" header into "Parameters"
17
+ - Changed
18
+ - Added "self format" pre-commit hook to format docstrings within this repo
19
+ with its own formatting logic
20
+
21
+ ## [0.1.8] - 2025-10-14
22
+
23
+ - Fixed
24
+ - Bug in `_fix_rst_backticks()` where backtick pairs spanning multiple lines
25
+ (e.g., multi-line external links) were incorrectly processed
26
+ - Added `(?!_)` lookahead to regex pattern to prevent matching trailing
27
+ backticks from cross-references (e.g., `` `text`_ ``)
28
+ - Changed
29
+ - Moved backtick fixing from line-by-line processing to whole-docstring
30
+ processing to correctly handle multi-line constructs
31
+ - REPL lines (starting with `>>> ` or `... `) are now protected with
32
+ placeholders during backtick fixing to preserve backticks in Python
33
+ examples
34
+ - Added
35
+ - Test cases for multi-line external links in
36
+ `test_fix_rst_backticks_cases()`
37
+ - Test cases for REPL lines with backticks in
38
+ `test_fix_rst_backticks_cases()`
39
+ - Full diff
40
+ - https://github.com/jsh9/format-docstring/compare/0.1.7...0.1.8
41
+
9
42
  ## [0.1.7] - 2025-10-14
10
43
 
11
44
  - Fixed
@@ -13,6 +46,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
13
46
  external links (e.g., `` `Python <https://example.org>`_ ``)
14
47
  - Refactored `_fix_rst_backticks()` to use pre-compiled regex pattern for
15
48
  better performance
49
+ - Full diff
50
+ - https://github.com/jsh9/format-docstring/compare/0.1.6...0.1.7
16
51
 
17
52
  ## [0.1.6] - 2025-10-13
18
53
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: format-docstring
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: A Python formatter to wrap/adjust docstring lines
5
5
  Author-email: jsh9 <25124332+jsh9@users.noreply.github.com>
6
6
  Maintainer-email: jsh9 <25124332+jsh9@users.noreply.github.com>
@@ -27,7 +27,6 @@ Description-Content-Type: text/markdown
27
27
  License-File: LICENSE
28
28
  Requires-Dist: jupyter-notebook-parser>=0.1.4
29
29
  Requires-Dist: click>=8.0
30
- Requires-Dist: docstring_parser_fork>=0.0.14
31
30
  Requires-Dist: tomli>=1.1.0; python_version < "3.11"
32
31
  Dynamic: license-file
33
32
 
@@ -50,6 +49,7 @@ A Python formatter to automatically format numpy-style docstrings.
50
49
  - [4. Usage](#4-usage)
51
50
  - [4.1. Command Line Interface](#41-command-line-interface)
52
51
  - [4.2. Pre-commit Hook](#42-pre-commit-hook)
52
+ - [4.3. Opting Out of Formatting](#43-opting-out-of-formatting)
53
53
  - [5. Configuration](#5-configuration)
54
54
  - [5.1. Command-Line Options](#51-command-line-options)
55
55
  - [5.2. Usage Examples](#52-usage-examples)
@@ -281,6 +281,19 @@ Then install the pre-commit hook:
281
281
  pre-commit install
282
282
  ```
283
283
 
284
+ ### 4.3. Opting Out of Formatting
285
+
286
+ Add a comment containing `no-format-docstring` on the same line as the closing
287
+ triple quotes to prevent the formatter from touching that docstring:
288
+ `""" ... """ # no-format-docstring`.
289
+
290
+ You can combine this "no-format-docstring" with other directives like "noqa".
291
+
292
+ Tip: If you only want to keep specific formatter changes inside a docstring,
293
+ first run `format-docstring`, accept the parts you like, revert the edits you
294
+ dislike, and then add an inline `# no-format-docstring` comment so future runs
295
+ leave that docstring untouched.
296
+
284
297
  ## 5. Configuration
285
298
 
286
299
  ### 5.1. Command-Line Options
@@ -291,6 +304,8 @@ pre-commit install
291
304
  default: `numpy`). Note: Currently only `numpy` style is fully supported.
292
305
  - `--fix-rst-backticks BOOL`: Automatically fix single backticks to double
293
306
  backticks per rST syntax (default: True)
307
+ - `--verbose CHOICE`: Logging detail level (`default` keeps the existing
308
+ behaviour, `diff` prints unified diffs when rewrites happen)
294
309
  - `--exclude TEXT`: Regex pattern to exclude files/directories (default:
295
310
  `\.git|\.tox|\.pytest_cache`)
296
311
  - `--config PATH`: Path to a `pyproject.toml` config file. If not specified,
@@ -311,6 +326,9 @@ format-docstring --line-length 72 src/
311
326
  # Format Jupyter notebooks excluding certain directories
312
327
  format-docstring-jupyter --exclude "\.git|\.venv|__pycache__" notebooks/
313
328
 
329
+ # Preview changes with unified diffs
330
+ format-docstring --verbose diff src/
331
+
314
332
  # Use a specific config file
315
333
  format-docstring --config path/to/pyproject.toml src/
316
334
 
@@ -332,6 +350,7 @@ line_length = 79
332
350
  docstring_style = "numpy"
333
351
  fix_rst_backticks = true
334
352
  exclude = "\\.git|\\.venv|__pycache__"
353
+ verbose = "default" # or "diff" to print unified diffs
335
354
  ```
336
355
 
337
356
  **Available options:**
@@ -344,6 +363,7 @@ exclude = "\\.git|\\.venv|__pycache__"
344
363
  backticks per rST syntax (default: `true`)
345
364
  - `exclude` (str): Regex pattern to exclude files/directories (default:
346
365
  `"\\.git|\\.tox|\\.pytest_cache"`)
366
+ - `verbose` (str): Logging detail level (`"default"` or `"diff"`)
347
367
 
348
368
  The tool searches for `pyproject.toml` starting from the target file/directory
349
369
  and walking up the parent directories until one is found.
@@ -17,6 +17,7 @@ A Python formatter to automatically format numpy-style docstrings.
17
17
  - [4. Usage](#4-usage)
18
18
  - [4.1. Command Line Interface](#41-command-line-interface)
19
19
  - [4.2. Pre-commit Hook](#42-pre-commit-hook)
20
+ - [4.3. Opting Out of Formatting](#43-opting-out-of-formatting)
20
21
  - [5. Configuration](#5-configuration)
21
22
  - [5.1. Command-Line Options](#51-command-line-options)
22
23
  - [5.2. Usage Examples](#52-usage-examples)
@@ -248,6 +249,19 @@ Then install the pre-commit hook:
248
249
  pre-commit install
249
250
  ```
250
251
 
252
+ ### 4.3. Opting Out of Formatting
253
+
254
+ Add a comment containing `no-format-docstring` on the same line as the closing
255
+ triple quotes to prevent the formatter from touching that docstring:
256
+ `""" ... """ # no-format-docstring`.
257
+
258
+ You can combine this "no-format-docstring" with other directives like "noqa".
259
+
260
+ Tip: If you only want to keep specific formatter changes inside a docstring,
261
+ first run `format-docstring`, accept the parts you like, revert the edits you
262
+ dislike, and then add an inline `# no-format-docstring` comment so future runs
263
+ leave that docstring untouched.
264
+
251
265
  ## 5. Configuration
252
266
 
253
267
  ### 5.1. Command-Line Options
@@ -258,6 +272,8 @@ pre-commit install
258
272
  default: `numpy`). Note: Currently only `numpy` style is fully supported.
259
273
  - `--fix-rst-backticks BOOL`: Automatically fix single backticks to double
260
274
  backticks per rST syntax (default: True)
275
+ - `--verbose CHOICE`: Logging detail level (`default` keeps the existing
276
+ behaviour, `diff` prints unified diffs when rewrites happen)
261
277
  - `--exclude TEXT`: Regex pattern to exclude files/directories (default:
262
278
  `\.git|\.tox|\.pytest_cache`)
263
279
  - `--config PATH`: Path to a `pyproject.toml` config file. If not specified,
@@ -278,6 +294,9 @@ format-docstring --line-length 72 src/
278
294
  # Format Jupyter notebooks excluding certain directories
279
295
  format-docstring-jupyter --exclude "\.git|\.venv|__pycache__" notebooks/
280
296
 
297
+ # Preview changes with unified diffs
298
+ format-docstring --verbose diff src/
299
+
281
300
  # Use a specific config file
282
301
  format-docstring --config path/to/pyproject.toml src/
283
302
 
@@ -299,6 +318,7 @@ line_length = 79
299
318
  docstring_style = "numpy"
300
319
  fix_rst_backticks = true
301
320
  exclude = "\\.git|\\.venv|__pycache__"
321
+ verbose = "default" # or "diff" to print unified diffs
302
322
  ```
303
323
 
304
324
  **Available options:**
@@ -311,6 +331,7 @@ exclude = "\\.git|\\.venv|__pycache__"
311
331
  backticks per rST syntax (default: `true`)
312
332
  - `exclude` (str): Regex pattern to exclude files/directories (default:
313
333
  `"\\.git|\\.tox|\\.pytest_cache"`)
334
+ - `verbose` (str): Logging detail level (`"default"` or `"diff"`)
314
335
 
315
336
  The tool searches for `pyproject.toml` starting from the target file/directory
316
337
  and walking up the parent directories until one is found.
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import difflib
3
4
  import re
5
+ import sys
4
6
  from pathlib import Path
5
7
  from typing import Any
6
8
 
@@ -12,10 +14,12 @@ class BaseFixer:
12
14
  self,
13
15
  path: str,
14
16
  exclude_pattern: str = r'\.git|\.tox|\.pytest_cache',
17
+ verbose: str = 'default',
15
18
  ) -> None:
16
19
  """Initialize the fixer with a path and optional exclude pattern."""
17
20
  self.path = path
18
21
  self.exclude_pattern = exclude_pattern
22
+ self.verbose = verbose
19
23
 
20
24
  def _get_files_to_process(
21
25
  self, directory: Path, pattern: str
@@ -28,6 +32,25 @@ class BaseFixer:
28
32
  if not should_exclude_file(f, self.exclude_pattern)
29
33
  ]
30
34
 
35
+ def _print_diff(self, filename: str, before: str, after: str) -> None:
36
+ """Print a unified diff when verbose mode is enabled."""
37
+ if self.verbose != 'diff':
38
+ return
39
+
40
+ diff = difflib.unified_diff(
41
+ before.splitlines(keepends=True),
42
+ after.splitlines(keepends=True),
43
+ fromfile=f'{filename} (before)',
44
+ tofile=f'{filename} (after)',
45
+ lineterm='',
46
+ )
47
+ diff_text = ''.join(diff)
48
+ if diff_text:
49
+ if not diff_text.endswith('\n'):
50
+ diff_text += '\n'
51
+
52
+ print(diff_text, file=sys.stderr, end='')
53
+
31
54
  def fix_one_directory_or_one_file(self) -> int:
32
55
  """
33
56
  Fix formatting in a single file or all Python files in a directory.
@@ -55,9 +78,10 @@ class BaseFixer:
55
78
 
56
79
 
57
80
  def should_exclude_file(file_path: Path, exclude_pattern: str) -> bool:
58
- """Return True if `file_path` matches the provided exclude regex.
81
+ """
82
+ Return True if ``file_path`` matches the provided exclude regex.
59
83
 
60
- If `exclude_pattern` is empty or invalid, no files are excluded.
84
+ If ``exclude_pattern`` is empty or invalid, no files are excluded.
61
85
  """
62
86
  if not exclude_pattern:
63
87
  return False
@@ -12,6 +12,40 @@ ModuleClassOrFunc = (
12
12
  ast.Module | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef
13
13
  )
14
14
 
15
+ NO_FORMAT_DOCSTRING_MARKER = 'no-format-docstring'
16
+
17
+
18
+ def _determine_newline(text: str) -> str:
19
+ r"""
20
+ Return the dominant newline style detected in ``text``.
21
+
22
+ Defaults to ``\n`` when no Windows/Mac classic newlines are present.
23
+ """
24
+ if '\r\n' in text:
25
+ return '\r\n'
26
+
27
+ if '\r' in text:
28
+ return '\r'
29
+
30
+ return '\n'
31
+
32
+
33
+ def _has_inline_no_format_comment(source_code: str, end_pos: int) -> bool:
34
+ """
35
+ Return True if the closing quotes share a line with the sentinel comment.
36
+ """
37
+ source_len = len(source_code)
38
+ line_end = source_code.find('\n', end_pos)
39
+ if line_end == -1:
40
+ line_end = source_len
41
+
42
+ same_line_segment = source_code[end_pos:line_end].lower()
43
+ hash_index = same_line_segment.find('#')
44
+ if hash_index == -1:
45
+ return False
46
+
47
+ return NO_FORMAT_DOCSTRING_MARKER in same_line_segment[hash_index:]
48
+
15
49
 
16
50
  def fix_src(
17
51
  source_code: str,
@@ -20,7 +54,8 @@ def fix_src(
20
54
  docstring_style: str = 'numpy',
21
55
  fix_rst_backticks: bool = True,
22
56
  ) -> str:
23
- """Return code with only docstrings updated to wrapped content.
57
+ """
58
+ Return code with only docstrings updated to wrapped content.
24
59
 
25
60
  Parameters
26
61
  ----------
@@ -31,21 +66,20 @@ def fix_src(
31
66
  docstring_style : str, default='numpy'
32
67
  The docstring style to target ('numpy' or 'google').
33
68
  fix_rst_backticks : bool, default=True
34
- If True, automatically fix single backticks to double backticks per
35
- rST syntax.
69
+ If True, automatically fix single backticks to double backticks per rST
70
+ syntax.
36
71
 
37
72
  Returns
38
73
  -------
39
74
  str
40
- The updated source code. Only docstring literals are changed; all
41
- other formatting is preserved.
75
+ The updated source code. Only docstring literals are changed; all other
76
+ formatting is preserved.
42
77
 
43
78
  Notes
44
79
  -----
45
- This function avoids ``ast.unparse`` and instead replaces docstring
46
- literal spans directly in the original text to preserve non-docstring
47
- formatting and comments.
48
-
80
+ This function avoids ``ast.unparse`` and instead replaces docstring literal
81
+ spans directly in the original text to preserve non-docstring formatting
82
+ and comments.
49
83
  """
50
84
  tree: ast.Module = ast.parse(source_code)
51
85
  line_starts: list[int] = calc_line_starts(source_code)
@@ -93,7 +127,8 @@ def fix_src(
93
127
 
94
128
 
95
129
  def calc_line_starts(source_code: str) -> list[int]:
96
- """Return starting offsets for each line in the source string.
130
+ """
131
+ Return starting offsets for each line in the source string.
97
132
 
98
133
  Parameters
99
134
  ----------
@@ -104,7 +139,6 @@ def calc_line_starts(source_code: str) -> list[int]:
104
139
  -------
105
140
  list[int]
106
141
  A list of absolute indices for the start of each line.
107
-
108
142
  """
109
143
  starts: list[int] = [0]
110
144
  for i, ch in enumerate(source_code):
@@ -122,7 +156,8 @@ def build_replacement_docstring(
122
156
  docstring_style: str = 'numpy',
123
157
  fix_rst_backticks: bool = True,
124
158
  ) -> tuple[int, int, str] | None:
125
- """Compute a single docstring replacement for the given node.
159
+ """
160
+ Compute a single docstring replacement for the given node.
126
161
 
127
162
  Parameters
128
163
  ----------
@@ -137,15 +172,14 @@ def build_replacement_docstring(
137
172
  docstring_style : str, default='numpy'
138
173
  The docstring style to target ('numpy' or 'google').
139
174
  fix_rst_backticks : bool, default=True
140
- If True, automatically fix single backticks to double backticks per
141
- rST syntax.
175
+ If True, automatically fix single backticks to double backticks per rST
176
+ syntax.
142
177
 
143
178
  Returns
144
179
  -------
145
180
  tuple[int, int, str] or None
146
- A tuple ``(start, end, new_literal)`` indicating the replacement
147
- range and text, or ``None`` if no change is needed.
148
-
181
+ A tuple ``(start, end, new_literal)`` indicating the replacement range
182
+ and text, or ``None`` if no change is needed.
149
183
  """
150
184
  docstring_obj: ast.Expr | None = find_docstring(node)
151
185
  if docstring_obj is None:
@@ -159,6 +193,9 @@ def build_replacement_docstring(
159
193
  end: int = calc_abs_pos(line_starts, val.end_lineno, val.end_col_offset) # type: ignore[arg-type] # noqa: LN002
160
194
  original_literal = source_code[start:end]
161
195
 
196
+ if _has_inline_no_format_comment(source_code, end):
197
+ return None
198
+
162
199
  doc: str | None = ast.get_docstring(node, clean=False)
163
200
  if doc is None:
164
201
  return None
@@ -200,7 +237,8 @@ def build_replacement_docstring(
200
237
 
201
238
 
202
239
  def find_docstring(node: ModuleClassOrFunc) -> ast.Expr | None:
203
- """Return the first statement if it is a string-literal docstring.
240
+ """
241
+ Return the first statement if it is a string-literal docstring.
204
242
 
205
243
  Parameters
206
244
  ----------
@@ -213,7 +251,6 @@ def find_docstring(node: ModuleClassOrFunc) -> ast.Expr | None:
213
251
  ast.Expr or None
214
252
  The ``ast.Expr`` node that holds the docstring literal, if present;
215
253
  otherwise ``None``.
216
-
217
254
  """
218
255
  body: list[ast.stmt] | None = getattr(node, 'body', None)
219
256
  if not body:
@@ -231,7 +268,8 @@ def find_docstring(node: ModuleClassOrFunc) -> ast.Expr | None:
231
268
 
232
269
 
233
270
  def calc_abs_pos(line_starts: list[int], lineno: int, col: int) -> int:
234
- """Convert a (lineno, col) pair to an absolute index.
271
+ """
272
+ Convert a (lineno, col) pair to an absolute index.
235
273
 
236
274
  Parameters
237
275
  ----------
@@ -246,19 +284,19 @@ def calc_abs_pos(line_starts: list[int], lineno: int, col: int) -> int:
246
284
  -------
247
285
  int
248
286
  The absolute character index into the source string.
249
-
250
287
  """
251
288
  return line_starts[lineno - 1] + col
252
289
 
253
290
 
254
291
  def rebuild_literal(original_literal: str, content: str) -> str | None:
255
- """Rebuild a string literal preserving prefix and quote style.
292
+ """
293
+ Rebuild a string literal preserving prefix and quote style.
256
294
 
257
295
  Parameters
258
296
  ----------
259
297
  original_literal : str
260
- The exact text of the original string literal including any prefix
261
- and surrounding quotes.
298
+ The exact text of the original string literal including any prefix and
299
+ surrounding quotes.
262
300
  content : str
263
301
  The new inner content (without surrounding quotes).
264
302
 
@@ -267,7 +305,6 @@ def rebuild_literal(original_literal: str, content: str) -> str | None:
267
305
  str or None
268
306
  A new literal string with the same prefix and quotes and the new
269
307
  content. Returns ``None`` if the original cannot be parsed.
270
-
271
308
  """
272
309
  i = 0
273
310
  n = len(original_literal)
@@ -286,6 +323,11 @@ def rebuild_literal(original_literal: str, content: str) -> str | None:
286
323
  else:
287
324
  return None
288
325
 
326
+ newline: str = _determine_newline(original_literal)
327
+ if newline != '\n':
328
+ normalized_content = content.replace('\r\n', '\n').replace('\r', '\n')
329
+ content = newline.join(normalized_content.split('\n'))
330
+
289
331
  return f'{prefix}{delim}{content}{delim}'
290
332
 
291
333
 
@@ -296,7 +338,8 @@ def wrap_docstring(
296
338
  leading_indent: int = 0,
297
339
  fix_rst_backticks: bool = True,
298
340
  ) -> str:
299
- """Wrap a docstring to the given line length (stub).
341
+ """
342
+ Wrap a docstring to the given line length (stub).
300
343
 
301
344
  Parameters
302
345
  ----------
@@ -309,8 +352,8 @@ def wrap_docstring(
309
352
  leading_indent : int, default=0
310
353
  The number of indentation spaces of this docstring.
311
354
  fix_rst_backticks : bool, default=True
312
- If True, automatically fix single backticks to double backticks per
313
- rST syntax.
355
+ If True, automatically fix single backticks to double backticks per rST
356
+ syntax.
314
357
 
315
358
  Returns
316
359
  -------
@@ -322,7 +365,6 @@ def wrap_docstring(
322
365
  This function dispatches to style-specific implementations:
323
366
  - 'numpy' -> wrap_docstring_numpy
324
367
  - 'google' -> wrap_docstring_google
325
-
326
368
  """
327
369
  style = (docstring_style or '').strip().lower()
328
370
  if style == 'google':