format-docstring 0.2.3__tar.gz → 0.2.5__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 (109) hide show
  1. {format_docstring-0.2.3 → format_docstring-0.2.5}/.gitignore +3 -0
  2. {format_docstring-0.2.3 → format_docstring-0.2.5}/.pre-commit-config.yaml +3 -4
  3. {format_docstring-0.2.3 → format_docstring-0.2.5}/CHANGELOG.md +18 -0
  4. {format_docstring-0.2.3 → format_docstring-0.2.5}/PKG-INFO +1 -1
  5. {format_docstring-0.2.3 → format_docstring-0.2.5}/format_docstring/line_wrap_numpy.py +105 -21
  6. {format_docstring-0.2.3 → format_docstring-0.2.5}/format_docstring.egg-info/PKG-INFO +1 -1
  7. {format_docstring-0.2.3 → format_docstring-0.2.5}/format_docstring.egg-info/SOURCES.txt +1 -0
  8. {format_docstring-0.2.3 → format_docstring-0.2.5}/pyproject.toml +1 -1
  9. format_docstring-0.2.5/tests/test_data/end_to_end/numpy/arg_name_is_default.txt +44 -0
  10. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/signature_sync_yields.txt +49 -1
  11. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_line_wrap_numpy.py +10 -0
  12. {format_docstring-0.2.3 → format_docstring-0.2.5}/.github/workflows/python-package.yml +0 -0
  13. {format_docstring-0.2.3 → format_docstring-0.2.5}/.github/workflows/python-publish.yml +0 -0
  14. {format_docstring-0.2.3 → format_docstring-0.2.5}/.pre-commit-hooks.yaml +0 -0
  15. {format_docstring-0.2.3 → format_docstring-0.2.5}/AGENTS.md +0 -0
  16. {format_docstring-0.2.3 → format_docstring-0.2.5}/LICENSE +0 -0
  17. {format_docstring-0.2.3 → format_docstring-0.2.5}/README.md +0 -0
  18. {format_docstring-0.2.3 → format_docstring-0.2.5}/format_docstring/__init__.py +0 -0
  19. {format_docstring-0.2.3 → format_docstring-0.2.5}/format_docstring/base_fixer.py +0 -0
  20. {format_docstring-0.2.3 → format_docstring-0.2.5}/format_docstring/config.py +0 -0
  21. {format_docstring-0.2.3 → format_docstring-0.2.5}/format_docstring/docstring_rewriter.py +0 -0
  22. {format_docstring-0.2.3 → format_docstring-0.2.5}/format_docstring/line_wrap_google.py +0 -0
  23. {format_docstring-0.2.3 → format_docstring-0.2.5}/format_docstring/line_wrap_utils.py +0 -0
  24. {format_docstring-0.2.3 → format_docstring-0.2.5}/format_docstring/main_jupyter.py +0 -0
  25. {format_docstring-0.2.3 → format_docstring-0.2.5}/format_docstring/main_py.py +0 -0
  26. {format_docstring-0.2.3 → format_docstring-0.2.5}/format_docstring.egg-info/dependency_links.txt +0 -0
  27. {format_docstring-0.2.3 → format_docstring-0.2.5}/format_docstring.egg-info/entry_points.txt +0 -0
  28. {format_docstring-0.2.3 → format_docstring-0.2.5}/format_docstring.egg-info/requires.txt +0 -0
  29. {format_docstring-0.2.3 → format_docstring-0.2.5}/format_docstring.egg-info/top_level.txt +0 -0
  30. {format_docstring-0.2.3 → format_docstring-0.2.5}/muff.toml +0 -0
  31. {format_docstring-0.2.3 → format_docstring-0.2.5}/requirements.dev +0 -0
  32. {format_docstring-0.2.3 → format_docstring-0.2.5}/setup.cfg +0 -0
  33. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/__init__.py +0 -0
  34. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/helpers.py +0 -0
  35. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_base_fixer.py +0 -0
  36. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_config.py +0 -0
  37. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/README.md +0 -0
  38. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/colon_spacing_fix.txt +0 -0
  39. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/contents_that_are_not_wrapped.txt +0 -0
  40. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/default_value_standardization.txt +0 -0
  41. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/empty_lines_are_respected.txt +0 -0
  42. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/examples_section.txt +0 -0
  43. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/existing_linebreaks_should_not_be_respected.txt +0 -0
  44. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/fix_rst_backticks.txt +0 -0
  45. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/four_level_nested_classes.txt +0 -0
  46. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/indent_four_levels_16_spaces_width_10.txt +0 -0
  47. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/indent_misaligned_all.txt +0 -0
  48. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/indent_two_levels_8_spaces.txt +0 -0
  49. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/line_length_2.txt +0 -0
  50. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/mismatched_underlines.txt +0 -0
  51. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/mismatched_underlines_one_dash.txt +0 -0
  52. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/mismatched_underlines_two_dashes.txt +0 -0
  53. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/module_level_docstring.txt +0 -0
  54. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/new_lines_before_and_after.txt +0 -0
  55. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/no_format_docstring_comment.txt +0 -0
  56. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/param_signature_without_type.txt +0 -0
  57. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/parameters_returns_raises_wrapping.txt +0 -0
  58. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/returns_signature_and_description.txt +0 -0
  59. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/section_headings_with_colons.txt +0 -0
  60. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/section_title_fixed.txt +0 -0
  61. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/sections_notes_examples.txt +0 -0
  62. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/signature_dont_sync_raises.txt +0 -0
  63. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/signature_line_is_not_wrapped.txt +0 -0
  64. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/signature_sync_class_docstrings.txt +0 -0
  65. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/signature_sync_parameters.txt +0 -0
  66. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/signature_sync_returns.txt +0 -0
  67. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/single_line_docstring.txt +0 -0
  68. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/texts_are_rewrapped.txt +0 -0
  69. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/end_to_end/numpy/very_long_unbreakable_word.txt +0 -0
  70. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/integration_test/numpy/after.ipynb +0 -0
  71. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/integration_test/numpy/after.py +0 -0
  72. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/integration_test/numpy/after_50.ipynb +0 -0
  73. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/integration_test/numpy/after_50.py +0 -0
  74. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/integration_test/numpy/before.ipynb +0 -0
  75. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/integration_test/numpy/before.py +0 -0
  76. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/jupyter/before.ipynb +0 -0
  77. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/jupyter/verbose_before.ipynb +0 -0
  78. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/README.md +0 -0
  79. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/colon_spacing_fix.txt +0 -0
  80. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/contents_that_are_not_wrapped.txt +0 -0
  81. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/default_value_standardization.txt +0 -0
  82. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/empty_lines_are_respected.txt +0 -0
  83. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/examples_section.txt +0 -0
  84. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/existing_linebreaks_should_not_be_respected.txt +0 -0
  85. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/fix_rst_backticks.txt +0 -0
  86. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/indent_four_levels_16_spaces_width_10.txt +0 -0
  87. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/indent_two_levels_8_spaces.txt +0 -0
  88. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/line_length_2.txt +0 -0
  89. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/mismatched_underlines.txt +0 -0
  90. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/mismatched_underlines_one_dash.txt +0 -0
  91. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/mismatched_underlines_two_dashes.txt +0 -0
  92. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/module_level_docstring.txt +0 -0
  93. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/param_signature_without_type.txt +0 -0
  94. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/parameters_returns_raises_wrapping.txt +0 -0
  95. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/returns_signature_and_description.txt +0 -0
  96. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/section_headings_with_colons.txt +0 -0
  97. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/section_title_fixed.txt +0 -0
  98. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/sections_notes_examples.txt +0 -0
  99. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/signature_line_is_not_wrapped.txt +0 -0
  100. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/texts_are_rewrapped.txt +0 -0
  101. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/line_wrap/numpy/very_long_unbreakable_word.txt +0 -0
  102. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_data/playground.py +0 -0
  103. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_docstring_rewriter.py +0 -0
  104. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_line_wrap_google.py +0 -0
  105. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_line_wrap_utils.py +0 -0
  106. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_main_jupyter.py +0 -0
  107. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_main_py.py +0 -0
  108. {format_docstring-0.2.3 → format_docstring-0.2.5}/tests/test_playground.py +0 -0
  109. {format_docstring-0.2.3 → format_docstring-0.2.5}/tox.ini +0 -0
@@ -208,3 +208,6 @@ __marimo__/
208
208
 
209
209
  # JetBrains IDEs
210
210
  .idea/
211
+
212
+ # VS Code
213
+ .vscode/
@@ -1,4 +1,5 @@
1
1
  ---
2
+ exclude: ^tests/test_data/
2
3
  repos:
3
4
  - repo: https://github.com/pre-commit/pre-commit-hooks
4
5
  rev: v6.0.0
@@ -13,7 +14,7 @@ repos:
13
14
  - id: check-yaml
14
15
  - id: name-tests-test
15
16
  args: [--pytest-test-first]
16
- exclude: ^tests/test_data/|^tests/helpers\.py$
17
+ exclude: ^tests/helpers\.py$
17
18
  - id: check-merge-conflict
18
19
  - repo: https://github.com/jsh9/muff-pre-commit
19
20
  rev: 0.13.2
@@ -65,7 +66,7 @@ repos:
65
66
  rev: 0.1.0
66
67
  hooks:
67
68
  - id: markdown-toc-creator
68
- exclude: ^tests/test_data/|^AGENTS\.md$|^CHANGELOG\.md$
69
+ exclude: ^AGENTS\.md$|^CHANGELOG\.md$
69
70
  - repo: https://github.com/jsh9/markdown-heading-numbering
70
71
  rev: 0.1.0
71
72
  hooks:
@@ -79,7 +80,6 @@ repos:
79
80
  args: [-m, format_docstring.main_py, --verbose, diff]
80
81
  language: python
81
82
  types: [python]
82
- exclude: ^tests/test_data/
83
83
  additional_dependencies:
84
84
  - click>=8.0
85
85
  - jupyter-notebook-parser>=0.1.4
@@ -90,7 +90,6 @@ repos:
90
90
  args: [-m, format_docstring.main_jupyter, --verbose, diff]
91
91
  language: python
92
92
  files: ^.*\.ipynb$
93
- exclude: ^tests/test_data/
94
93
  additional_dependencies:
95
94
  - click>=8.0
96
95
  - jupyter-notebook-parser>=0.1.4
@@ -6,6 +6,24 @@ 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.2.5] - 2025-11-20
10
+
11
+ - Fixed
12
+ - A bug where `Generator[XXX, YYY, ZZZ]` in the return type annotation is not
13
+ parsed correctly (the Yields section should have been XXX)
14
+ - Full diff
15
+ - https://github.com/jsh9/format-docstring/compare/0.2.4...0.2.5
16
+
17
+ ## [0.2.4] - 2025-10-27
18
+
19
+ - Fixed
20
+ - A bug where 2nd pair of backticks can't be added for dunder names (such as
21
+ `__init__`)
22
+ - A bug where input args named `default` would get treated incorrectly (the
23
+ tool would confuse it with the default values)
24
+ - Full diff
25
+ - https://github.com/jsh9/format-docstring/compare/0.2.3...0.2.4
26
+
9
27
  ## [0.2.3] - 2025-10-22
10
28
 
11
29
  - Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: format-docstring
3
- Version: 0.2.3
3
+ Version: 0.2.5
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>
@@ -197,6 +197,7 @@ def wrap_docstring_numpy( # noqa: C901, PLR0915, TODO: https://github.com/jsh9/
197
197
 
198
198
  # Treat top-level lines as signatures
199
199
  if leading_indent is None or indent_length <= leading_indent:
200
+ is_yields_section = section_lower_case.startswith('yield')
200
201
  if not return_signature_style_determined:
201
202
  return_use_multiple_signatures = (
202
203
  _detect_multiple_return_signatures(
@@ -223,6 +224,12 @@ def wrap_docstring_numpy( # noqa: C901, PLR0915, TODO: https://github.com/jsh9/
223
224
  # Fallback to last component when docstring expects more
224
225
  desired_annotation = return_components[-1]
225
226
 
227
+ if is_yields_section:
228
+ desired_annotation = (
229
+ _unwrap_generator_annotation(desired_annotation)
230
+ or desired_annotation
231
+ )
232
+
226
233
  if desired_annotation is None:
227
234
  temp_out.append(line)
228
235
  i += 1
@@ -464,19 +471,39 @@ def _standardize_default_value(line: str) -> str:
464
471
  >>> _standardize_default_value('arg : bool, default: True')
465
472
  'arg : bool, default=True'
466
473
  """
474
+ colon_idx = line.find(':')
475
+ if colon_idx == -1:
476
+ return line
477
+
478
+ # `prefix` is everything before the 1st colon (param identifier portion).
479
+ # We leave `prefix` untouched so arg names like `default` aren't rewritten.
480
+ prefix = line[: colon_idx + 1]
481
+ after_colon = line[colon_idx + 1 :]
482
+
467
483
  # Check colon format first to avoid matching colons in space-based pattern
468
- match = _DEFAULT_COLON_RE.match(line)
484
+ match = _DEFAULT_COLON_RE.match(after_colon)
469
485
  if match:
470
- before = match.group(1).rstrip()
486
+ before = match.group(1)
487
+ if before.strip() == '':
488
+ return line
489
+
471
490
  default_value = match.group(2).strip()
472
- return f'{before}, default={default_value}'
491
+ rebuilt_suffix = f'{before.rstrip()}, default={default_value}'
492
+ return f'{prefix}{rebuilt_suffix}'
473
493
 
474
494
  # Try space-separated format with optional "is"
475
- match = _DEFAULT_SPACE_RE.match(line)
495
+ match = _DEFAULT_SPACE_RE.match(after_colon)
476
496
  if match:
477
- before = match.group(1).rstrip()
497
+ before = match.group(1)
498
+ if before.strip() == '':
499
+ return line
500
+
501
+ # ``before`` still contains any annotation text; tightening the spacing
502
+ # here standardizes the ``", default=..."`` suffix while preserving
503
+ # whatever appeared to the left.
478
504
  default_value = match.group(2).strip()
479
- return f'{before}, default={default_value}'
505
+ rebuilt_suffix = f'{before.rstrip()}, default={default_value}'
506
+ return f'{prefix}{rebuilt_suffix}'
480
507
 
481
508
  return line
482
509
 
@@ -598,19 +625,6 @@ def _split_tuple_annotation(annotation: str | None) -> list[str] | None:
598
625
  except (SyntaxError, ValueError):
599
626
  return None
600
627
 
601
- def _name_of(node: ast.AST) -> str | None:
602
- if isinstance(node, ast.Name):
603
- return node.id
604
-
605
- if isinstance(node, ast.Attribute):
606
- base = _name_of(node.value)
607
- if base is None:
608
- return None
609
-
610
- return f'{base}.{node.attr}'
611
-
612
- return None
613
-
614
628
  if isinstance(expr, ast.Subscript):
615
629
  base_name = _name_of(expr.value)
616
630
  if base_name not in {'tuple', 'Tuple'}:
@@ -637,6 +651,63 @@ def _split_tuple_annotation(annotation: str | None) -> list[str] | None:
637
651
  return None
638
652
 
639
653
 
654
+ def _name_of(node: ast.AST) -> str | None:
655
+ """
656
+ Return the dotted name represented by ``node`` if possible.
657
+ """
658
+ if isinstance(node, ast.Name):
659
+ return node.id
660
+
661
+ if isinstance(node, ast.Attribute):
662
+ base = _name_of(node.value)
663
+ if base is None:
664
+ return None
665
+
666
+ return f'{base}.{node.attr}'
667
+
668
+ return None
669
+
670
+
671
+ def _unwrap_generator_annotation(annotation: str | None) -> str | None:
672
+ """
673
+ Return the first yield type when ``annotation`` is a Generator or
674
+ AsyncGenerator.
675
+
676
+ This is a small helper to keep ``Yields`` sections intuitive; Python
677
+ signatures often annotate generator functions as ``Generator[T, None,
678
+ None]`` but docstrings should spell out the yielded type ``T`` instead of
679
+ the whole container.
680
+ """
681
+ if annotation is None:
682
+ return None
683
+
684
+ try:
685
+ expr = ast.parse(annotation, mode='eval').body
686
+ except (SyntaxError, ValueError):
687
+ return None
688
+
689
+ if not isinstance(expr, ast.Subscript):
690
+ return None
691
+
692
+ base_name = _name_of(expr.value)
693
+ if base_name is None or base_name.split('.')[-1] not in {
694
+ 'Generator',
695
+ 'AsyncGenerator',
696
+ }:
697
+ return None
698
+
699
+ slice_node = expr.slice
700
+ if not isinstance(slice_node, ast.Tuple) or not slice_node.elts:
701
+ return None
702
+
703
+ first = slice_node.elts[0]
704
+ segment = ast.get_source_segment(annotation, first)
705
+ if segment is None:
706
+ segment = ast.unparse(first)
707
+
708
+ return segment.strip()
709
+
710
+
640
711
  def _detect_multiple_return_signatures(
641
712
  lines: list[str],
642
713
  start_idx: int,
@@ -730,12 +801,20 @@ def handle_single_line_docstring(
730
801
  # or certain punctuation (like > and . for `>>> ` and `... ` literals)
731
802
  # Note: We match [^`]+ (anything except backticks) and then check in the
732
803
  # replacement function whether it's an external link (contains < followed by >)
733
- # The opening backtick must not be immediately followed by _ or __ (to avoid
734
- # matching the trailing backtick of cross-references like `text`_ or `text`__)
735
804
  _RST_BACKTICK_PATTERN = re.compile(
736
805
  r'(?:^|(?<=\s)|(?<=\()|(?<=[>.]))(?::[\w-]+:)?`(?!_)([^`]+)`(?!`)(?!__)(?!_)'
737
806
  )
738
807
 
808
+ # 2nd-stage fixer for ``__dunder__`` names that slipped past the main pattern
809
+ # because the literal starts with an underscore. Negative lookbehinds/aheads
810
+ # ensure we only touch isolated single-backtick literals and leave
811
+ # cross-references (`name`_ / `name`__) alone.
812
+ _DUNDER_LITERAL_PATTERN = re.compile(
813
+ r'(?<!`)`(__[A-Za-z0-9_]+__)`(?!`)(?!_)(?!__)'
814
+ )
815
+ # Replacement wraps the captured dunder name (group 1) with double backticks.
816
+ _DUNDER_LITERAL_REPLACEMENT = r'``\1``'
817
+
739
818
 
740
819
  def _fix_rst_backticks(docstring: str) -> str:
741
820
  """
@@ -857,6 +936,11 @@ def _fix_rst_backticks(docstring: str) -> str:
857
936
  # Process the entire docstring (with REPL lines protected)
858
937
  protected_docstring = ''.join(protected_lines)
859
938
  processed = _RST_BACKTICK_PATTERN.sub(replace_func, protected_docstring)
939
+ # Upgrade remaining single-backtick ``__dunder__`` literals to double
940
+ # backticks; they are safe literals (not targets or refs) after the guards.
941
+ processed = _DUNDER_LITERAL_PATTERN.sub(
942
+ _DUNDER_LITERAL_REPLACEMENT, processed
943
+ )
860
944
 
861
945
  # Restore REPL lines
862
946
  result_lines = processed.splitlines(keepends=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: format-docstring
3
- Version: 0.2.3
3
+ Version: 0.2.5
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>
@@ -39,6 +39,7 @@ tests/test_main_py.py
39
39
  tests/test_playground.py
40
40
  tests/test_data/playground.py
41
41
  tests/test_data/end_to_end/numpy/README.md
42
+ tests/test_data/end_to_end/numpy/arg_name_is_default.txt
42
43
  tests/test_data/end_to_end/numpy/colon_spacing_fix.txt
43
44
  tests/test_data/end_to_end/numpy/contents_that_are_not_wrapped.txt
44
45
  tests/test_data/end_to_end/numpy/default_value_standardization.txt
@@ -4,7 +4,7 @@ requires = ["setuptools-scm[toml]>=6.2", "setuptools>=45"]
4
4
 
5
5
  [project]
6
6
  name = "format-docstring"
7
- version = "0.2.3"
7
+ version = "0.2.5"
8
8
  dependencies = [
9
9
  "click>=8.0",
10
10
  "jupyter-notebook-parser>=0.1.4",
@@ -0,0 +1,44 @@
1
+ LINE_LENGTH: 79
2
+
3
+ **********
4
+ def func1(arg1: str, default: int = 2, _default: str = "value", default_: list[int] | None = None) -> None:
5
+ """
6
+ Do something
7
+
8
+ Parameters
9
+ ----------
10
+ arg1 : str
11
+ Very very very very very very very very very very very very very very very very
12
+ very very very very very very very very very very very very very very very very
13
+ very very very very very very very
14
+ default : int
15
+ Quite quite quite quite quite quite quite quite quite quite quite quite quite quite quite quite
16
+ quite quite quite quite quite quite quite quite quite quite quite quite quite quite quite quite long
17
+ _default : str default is "value"
18
+ Placeholder description for _default parameter.
19
+ default_ : list[int] | None default None
20
+ Placeholder description for default_ parameter.
21
+ """
22
+ pass
23
+
24
+ **********
25
+ def func1(arg1: str, default: int = 2, _default: str = "value", default_: list[int] | None = None) -> None:
26
+ """
27
+ Do something
28
+
29
+ Parameters
30
+ ----------
31
+ arg1 : str
32
+ Very very very very very very very very very very very very very very
33
+ very very very very very very very very very very very very very very
34
+ very very very very very very very very very very very
35
+ default : int, default=2
36
+ Quite quite quite quite quite quite quite quite quite quite quite quite
37
+ quite quite quite quite quite quite quite quite quite quite quite quite
38
+ quite quite quite quite quite quite quite quite long
39
+ _default : str, default="value"
40
+ Placeholder description for _default parameter.
41
+ default_ : list[int] | None, default=None
42
+ Placeholder description for default_ parameter.
43
+ """
44
+ pass
@@ -51,6 +51,30 @@ def yield_custom_type_2() -> Iterator[tuple['MyType1', "MyType2"]]:
51
51
  yield 'value'
52
52
  yield 1
53
53
 
54
+
55
+ def yield_middle_non_none() -> Generator[int, str, None]:
56
+ """
57
+ Yield with send type.
58
+
59
+ Yields
60
+ ------
61
+ str
62
+ Should match first annotation element, not send type.
63
+ """
64
+ yield 1
65
+
66
+
67
+ def yield_last_non_none() -> Generator[int, None, str]:
68
+ """
69
+ Yield with return type.
70
+
71
+ Yields
72
+ ------
73
+ str
74
+ Should match first annotation element, not return type.
75
+ """
76
+ yield 1
77
+
54
78
  **********
55
79
  from collections.abc import Generator, Iterator
56
80
 
@@ -73,7 +97,7 @@ def yield_named() -> Generator[int, None, None]:
73
97
 
74
98
  Yields
75
99
  ------
76
- result : Generator[int, None, None]
100
+ result : int
77
101
  Should match annotation.
78
102
  """
79
103
  yield 1
@@ -101,3 +125,27 @@ def yield_custom_type_2() -> Iterator[tuple['MyType1', "MyType2"]]:
101
125
  """
102
126
  yield 'value'
103
127
  yield 1
128
+
129
+
130
+ def yield_middle_non_none() -> Generator[int, str, None]:
131
+ """
132
+ Yield with send type.
133
+
134
+ Yields
135
+ ------
136
+ int
137
+ Should match first annotation element, not send type.
138
+ """
139
+ yield 1
140
+
141
+
142
+ def yield_last_non_none() -> Generator[int, None, str]:
143
+ """
144
+ Yield with return type.
145
+
146
+ Yields
147
+ ------
148
+ int
149
+ Should match first annotation element, not return type.
150
+ """
151
+ yield 1
@@ -238,6 +238,8 @@ def test_fix_colon_spacing(line: str, expected: str) -> None:
238
238
  ('arg : int', 'arg : int'),
239
239
  ('arg : str', 'arg : str'),
240
240
  (' arg : bool', ' arg : bool'),
241
+ ('default : int', 'default : int'),
242
+ (' default : int', ' default : int'),
241
243
  # Case insensitive "default"
242
244
  ('arg : int Default 10', 'arg : int, default=10'),
243
245
  ('arg : int DEFAULT 10', 'arg : int, default=10'),
@@ -262,6 +264,14 @@ def test_standardize_default_value(line: str, expected: str) -> None:
262
264
  'Underscores inside literal are fine: `foo_bar`.',
263
265
  'Underscores inside literal are fine: ``foo_bar``.',
264
266
  ),
267
+ (
268
+ 'Dunder names should be wrapped: `__init__`',
269
+ 'Dunder names should be wrapped: ``__init__``',
270
+ ),
271
+ (
272
+ 'Dunder names should be wrapped: `__init123__`',
273
+ 'Dunder names should be wrapped: ``__init123__``',
274
+ ),
265
275
  (
266
276
  'Adjacent to parentheses: (`call_me`) and `ok`',
267
277
  'Adjacent to parentheses: (``call_me``) and ``ok``',