format-docstring 0.2.4__tar.gz → 0.2.6__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 (112) hide show
  1. {format_docstring-0.2.4 → format_docstring-0.2.6}/.gitignore +3 -0
  2. {format_docstring-0.2.4 → format_docstring-0.2.6}/CHANGELOG.md +21 -0
  3. {format_docstring-0.2.4 → format_docstring-0.2.6}/PKG-INFO +1 -1
  4. {format_docstring-0.2.4 → format_docstring-0.2.6}/format_docstring/docstring_rewriter.py +9 -2
  5. {format_docstring-0.2.4 → format_docstring-0.2.6}/format_docstring/line_wrap_numpy.py +103 -25
  6. {format_docstring-0.2.4 → format_docstring-0.2.6}/format_docstring.egg-info/PKG-INFO +1 -1
  7. {format_docstring-0.2.4 → format_docstring-0.2.6}/format_docstring.egg-info/SOURCES.txt +3 -0
  8. {format_docstring-0.2.4 → format_docstring-0.2.6}/pyproject.toml +1 -1
  9. format_docstring-0.2.6/tests/test_data/end_to_end/numpy/rST_cross_reference.txt +72 -0
  10. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/signature_sync_yields.txt +49 -1
  11. format_docstring-0.2.6/tests/test_data/end_to_end/numpy/variadic_signature_without_colon.txt +58 -0
  12. format_docstring-0.2.6/tests/test_data/line_wrap/numpy/variadic_signature_without_colon.txt +38 -0
  13. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_docstring_rewriter.py +52 -4
  14. {format_docstring-0.2.4 → format_docstring-0.2.6}/.github/workflows/python-package.yml +0 -0
  15. {format_docstring-0.2.4 → format_docstring-0.2.6}/.github/workflows/python-publish.yml +0 -0
  16. {format_docstring-0.2.4 → format_docstring-0.2.6}/.pre-commit-config.yaml +0 -0
  17. {format_docstring-0.2.4 → format_docstring-0.2.6}/.pre-commit-hooks.yaml +0 -0
  18. {format_docstring-0.2.4 → format_docstring-0.2.6}/AGENTS.md +0 -0
  19. {format_docstring-0.2.4 → format_docstring-0.2.6}/LICENSE +0 -0
  20. {format_docstring-0.2.4 → format_docstring-0.2.6}/README.md +0 -0
  21. {format_docstring-0.2.4 → format_docstring-0.2.6}/format_docstring/__init__.py +0 -0
  22. {format_docstring-0.2.4 → format_docstring-0.2.6}/format_docstring/base_fixer.py +0 -0
  23. {format_docstring-0.2.4 → format_docstring-0.2.6}/format_docstring/config.py +0 -0
  24. {format_docstring-0.2.4 → format_docstring-0.2.6}/format_docstring/line_wrap_google.py +0 -0
  25. {format_docstring-0.2.4 → format_docstring-0.2.6}/format_docstring/line_wrap_utils.py +0 -0
  26. {format_docstring-0.2.4 → format_docstring-0.2.6}/format_docstring/main_jupyter.py +0 -0
  27. {format_docstring-0.2.4 → format_docstring-0.2.6}/format_docstring/main_py.py +0 -0
  28. {format_docstring-0.2.4 → format_docstring-0.2.6}/format_docstring.egg-info/dependency_links.txt +0 -0
  29. {format_docstring-0.2.4 → format_docstring-0.2.6}/format_docstring.egg-info/entry_points.txt +0 -0
  30. {format_docstring-0.2.4 → format_docstring-0.2.6}/format_docstring.egg-info/requires.txt +0 -0
  31. {format_docstring-0.2.4 → format_docstring-0.2.6}/format_docstring.egg-info/top_level.txt +0 -0
  32. {format_docstring-0.2.4 → format_docstring-0.2.6}/muff.toml +0 -0
  33. {format_docstring-0.2.4 → format_docstring-0.2.6}/requirements.dev +0 -0
  34. {format_docstring-0.2.4 → format_docstring-0.2.6}/setup.cfg +0 -0
  35. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/__init__.py +0 -0
  36. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/helpers.py +0 -0
  37. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_base_fixer.py +0 -0
  38. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_config.py +0 -0
  39. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/README.md +0 -0
  40. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/arg_name_is_default.txt +0 -0
  41. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/colon_spacing_fix.txt +0 -0
  42. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/contents_that_are_not_wrapped.txt +0 -0
  43. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/default_value_standardization.txt +0 -0
  44. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/empty_lines_are_respected.txt +0 -0
  45. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/examples_section.txt +0 -0
  46. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/existing_linebreaks_should_not_be_respected.txt +0 -0
  47. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/fix_rst_backticks.txt +0 -0
  48. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/four_level_nested_classes.txt +0 -0
  49. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/indent_four_levels_16_spaces_width_10.txt +0 -0
  50. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/indent_misaligned_all.txt +0 -0
  51. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/indent_two_levels_8_spaces.txt +0 -0
  52. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/line_length_2.txt +0 -0
  53. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/mismatched_underlines.txt +0 -0
  54. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/mismatched_underlines_one_dash.txt +0 -0
  55. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/mismatched_underlines_two_dashes.txt +0 -0
  56. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/module_level_docstring.txt +0 -0
  57. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/new_lines_before_and_after.txt +0 -0
  58. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/no_format_docstring_comment.txt +0 -0
  59. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/param_signature_without_type.txt +0 -0
  60. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/parameters_returns_raises_wrapping.txt +0 -0
  61. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/returns_signature_and_description.txt +0 -0
  62. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/section_headings_with_colons.txt +0 -0
  63. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/section_title_fixed.txt +0 -0
  64. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/sections_notes_examples.txt +0 -0
  65. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/signature_dont_sync_raises.txt +0 -0
  66. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/signature_line_is_not_wrapped.txt +0 -0
  67. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/signature_sync_class_docstrings.txt +0 -0
  68. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/signature_sync_parameters.txt +0 -0
  69. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/signature_sync_returns.txt +0 -0
  70. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/single_line_docstring.txt +0 -0
  71. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/texts_are_rewrapped.txt +0 -0
  72. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/end_to_end/numpy/very_long_unbreakable_word.txt +0 -0
  73. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/integration_test/numpy/after.ipynb +0 -0
  74. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/integration_test/numpy/after.py +0 -0
  75. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/integration_test/numpy/after_50.ipynb +0 -0
  76. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/integration_test/numpy/after_50.py +0 -0
  77. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/integration_test/numpy/before.ipynb +0 -0
  78. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/integration_test/numpy/before.py +0 -0
  79. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/jupyter/before.ipynb +0 -0
  80. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/jupyter/verbose_before.ipynb +0 -0
  81. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/README.md +0 -0
  82. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/colon_spacing_fix.txt +0 -0
  83. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/contents_that_are_not_wrapped.txt +0 -0
  84. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/default_value_standardization.txt +0 -0
  85. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/empty_lines_are_respected.txt +0 -0
  86. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/examples_section.txt +0 -0
  87. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/existing_linebreaks_should_not_be_respected.txt +0 -0
  88. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/fix_rst_backticks.txt +0 -0
  89. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/indent_four_levels_16_spaces_width_10.txt +0 -0
  90. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/indent_two_levels_8_spaces.txt +0 -0
  91. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/line_length_2.txt +0 -0
  92. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/mismatched_underlines.txt +0 -0
  93. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/mismatched_underlines_one_dash.txt +0 -0
  94. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/mismatched_underlines_two_dashes.txt +0 -0
  95. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/module_level_docstring.txt +0 -0
  96. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/param_signature_without_type.txt +0 -0
  97. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/parameters_returns_raises_wrapping.txt +0 -0
  98. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/returns_signature_and_description.txt +0 -0
  99. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/section_headings_with_colons.txt +0 -0
  100. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/section_title_fixed.txt +0 -0
  101. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/sections_notes_examples.txt +0 -0
  102. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/signature_line_is_not_wrapped.txt +0 -0
  103. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/texts_are_rewrapped.txt +0 -0
  104. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/line_wrap/numpy/very_long_unbreakable_word.txt +0 -0
  105. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_data/playground.py +0 -0
  106. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_line_wrap_google.py +0 -0
  107. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_line_wrap_numpy.py +0 -0
  108. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_line_wrap_utils.py +0 -0
  109. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_main_jupyter.py +0 -0
  110. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_main_py.py +0 -0
  111. {format_docstring-0.2.4 → format_docstring-0.2.6}/tests/test_playground.py +0 -0
  112. {format_docstring-0.2.4 → format_docstring-0.2.6}/tox.ini +0 -0
@@ -208,3 +208,6 @@ __marimo__/
208
208
 
209
209
  # JetBrains IDEs
210
210
  .idea/
211
+
212
+ # VS Code
213
+ .vscode/
@@ -6,6 +6,27 @@ 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.6] - 2025-11-21
10
+
11
+ - Fixed
12
+ - Multiline annotations now normalize without inserting spaces inside
13
+ `Literal[...]` or other bracketed signatures, even when they span several
14
+ lines.
15
+ - A bug where bare `*args`/`**kwargs` signature lines (typed or untyped)
16
+ would be merged into previous descriptions
17
+ - NumPy signature syncing left the `:class:` / `:meth:` role prefixes behind,
18
+ producing mismatched annotations like ` : MyClass`
19
+ - Full diff
20
+ - https://github.com/jsh9/format-docstring/compare/0.2.5...0.2.6
21
+
22
+ ## [0.2.5] - 2025-11-20
23
+
24
+ - Fixed
25
+ - A bug where `Generator[XXX, YYY, ZZZ]` in the return type annotation is not
26
+ parsed correctly (the Yields section should have been XXX)
27
+ - Full diff
28
+ - https://github.com/jsh9/format-docstring/compare/0.2.4...0.2.5
29
+
9
30
  ## [0.2.4] - 2025-10-27
10
31
 
11
32
  - Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: format-docstring
3
- Version: 0.2.4
3
+ Version: 0.2.6
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>
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import ast
4
4
  import io
5
5
  import operator
6
+ import textwrap
6
7
  import tokenize
7
8
  from typing import TYPE_CHECKING
8
9
 
@@ -83,12 +84,18 @@ def _normalize_signature_segment(segment: str | None) -> str | None:
83
84
 
84
85
  normalized: str = segment.strip()
85
86
  if '\n' in normalized or '\r' in normalized or '\t' in normalized:
87
+ dedented: str = textwrap.dedent(normalized)
88
+ # Wrap in parentheses so unions split across lines
89
+ # (e.g. ``Literal[...] | None``)
90
+ # remain valid ``eval`` expressions even when indentation is uneven.
91
+ wrapped_for_parse = f'({dedented})'
92
+
86
93
  # `ast.unparse(ast.parse(...))` neatly flattens whitespace but it also
87
94
  # canonicalises string quotes to single quotes. We still rely on it for
88
95
  # whitespace normalization, so capture its output first.
89
96
  try:
90
- canonical = ast.unparse(ast.parse(normalized))
91
- except (SyntaxError, ValueError):
97
+ canonical = ast.unparse(ast.parse(wrapped_for_parse, mode='eval'))
98
+ except (SyntaxError, ValueError, IndentationError):
92
99
  return ' '.join(normalized.split())
93
100
 
94
101
  # Remember the exact string literal tokens from the original text. The
@@ -170,18 +170,22 @@ def wrap_docstring_numpy( # noqa: C901, PLR0915, TODO: https://github.com/jsh9/
170
170
  # section (indentation < 4). This prevents mis-detecting
171
171
  # description lines that happen to contain a colon (e.g., tables,
172
172
  # examples, notes) as new parameter signatures.
173
- if _is_param_signature(line) and (
174
- leading_indent is None or indent_length <= leading_indent
175
- ):
176
- fixed_line = _fix_colon_spacing(line)
177
- fixed_line = _standardize_default_value(fixed_line)
178
- fixed_line = _rewrite_parameter_signature(
179
- fixed_line, metadata_for_section
180
- )
181
- fixed_line = _standardize_default_value(fixed_line)
182
- temp_out.append(fixed_line)
183
- i += 1
184
- continue
173
+ if leading_indent is None or indent_length <= leading_indent:
174
+ if _is_param_signature(line):
175
+ fixed_line = _fix_colon_spacing(line)
176
+ fixed_line = _standardize_default_value(fixed_line)
177
+ fixed_line = _rewrite_parameter_signature(
178
+ fixed_line, metadata_for_section
179
+ )
180
+ fixed_line = _standardize_default_value(fixed_line)
181
+ temp_out.append(fixed_line)
182
+ i += 1
183
+ continue
184
+
185
+ if _is_bare_variadic_signature(line):
186
+ temp_out.append(line)
187
+ i += 1
188
+ continue
185
189
 
186
190
  # Description lines (typically indented): wrap if too long
187
191
  collect_to_temp_output(temp_out, line)
@@ -197,6 +201,7 @@ def wrap_docstring_numpy( # noqa: C901, PLR0915, TODO: https://github.com/jsh9/
197
201
 
198
202
  # Treat top-level lines as signatures
199
203
  if leading_indent is None or indent_length <= leading_indent:
204
+ is_yields_section = section_lower_case.startswith('yield')
200
205
  if not return_signature_style_determined:
201
206
  return_use_multiple_signatures = (
202
207
  _detect_multiple_return_signatures(
@@ -223,6 +228,12 @@ def wrap_docstring_numpy( # noqa: C901, PLR0915, TODO: https://github.com/jsh9/
223
228
  # Fallback to last component when docstring expects more
224
229
  desired_annotation = return_components[-1]
225
230
 
231
+ if is_yields_section:
232
+ desired_annotation = (
233
+ _unwrap_generator_annotation(desired_annotation)
234
+ or desired_annotation
235
+ )
236
+
226
237
  if desired_annotation is None:
227
238
  temp_out.append(line)
228
239
  i += 1
@@ -334,6 +345,13 @@ _PARAM_SIGNATURE_RE = re.compile(
334
345
  rf'^\s*\*{{0,2}}{START}{CONT}*(?:\s*,\s*\*{{0,2}}{START}{CONT}*)*\s*:\s*.*$'
335
346
  )
336
347
 
348
+ # Matches bare variadic signatures without a colon, e.g. ``**kwargs`` or
349
+ # ``*args, **kwargs``. These should be treated like signatures so description
350
+ # text doesn't get collapsed into the preceding entry.
351
+ _BARE_VARIADIC_SIGNATURE_RE = re.compile(
352
+ rf'^\s*\*{{1,2}}{START}{CONT}*(?:\s*,\s*\*{{1,2}}{START}{CONT}*)*\s*$'
353
+ )
354
+
337
355
 
338
356
  def _is_param_signature(text: str) -> bool:
339
357
  r"""
@@ -364,6 +382,16 @@ def _is_param_signature(text: str) -> bool:
364
382
  return bool(_PARAM_SIGNATURE_RE.match(text))
365
383
 
366
384
 
385
+ def _is_bare_variadic_signature(text: str) -> bool:
386
+ """
387
+ Return True for variadic parameter lines lacking ``:`` annotations.
388
+
389
+ Handles stripped signatures such as ``**kwargs`` so they are preserved as
390
+ their own logical entries inside ``Parameters`` sections.
391
+ """
392
+ return bool(_BARE_VARIADIC_SIGNATURE_RE.match(text))
393
+
394
+
367
395
  def _fix_colon_spacing(line: str) -> str:
368
396
  """
369
397
  Fix spacing around colons in parameter signature lines.
@@ -618,19 +646,6 @@ def _split_tuple_annotation(annotation: str | None) -> list[str] | None:
618
646
  except (SyntaxError, ValueError):
619
647
  return None
620
648
 
621
- def _name_of(node: ast.AST) -> str | None:
622
- if isinstance(node, ast.Name):
623
- return node.id
624
-
625
- if isinstance(node, ast.Attribute):
626
- base = _name_of(node.value)
627
- if base is None:
628
- return None
629
-
630
- return f'{base}.{node.attr}'
631
-
632
- return None
633
-
634
649
  if isinstance(expr, ast.Subscript):
635
650
  base_name = _name_of(expr.value)
636
651
  if base_name not in {'tuple', 'Tuple'}:
@@ -657,6 +672,63 @@ def _split_tuple_annotation(annotation: str | None) -> list[str] | None:
657
672
  return None
658
673
 
659
674
 
675
+ def _name_of(node: ast.AST) -> str | None:
676
+ """
677
+ Return the dotted name represented by ``node`` if possible.
678
+ """
679
+ if isinstance(node, ast.Name):
680
+ return node.id
681
+
682
+ if isinstance(node, ast.Attribute):
683
+ base = _name_of(node.value)
684
+ if base is None:
685
+ return None
686
+
687
+ return f'{base}.{node.attr}'
688
+
689
+ return None
690
+
691
+
692
+ def _unwrap_generator_annotation(annotation: str | None) -> str | None:
693
+ """
694
+ Return the first yield type when ``annotation`` is a Generator or
695
+ AsyncGenerator.
696
+
697
+ This is a small helper to keep ``Yields`` sections intuitive; Python
698
+ signatures often annotate generator functions as ``Generator[T, None,
699
+ None]`` but docstrings should spell out the yielded type ``T`` instead of
700
+ the whole container.
701
+ """
702
+ if annotation is None:
703
+ return None
704
+
705
+ try:
706
+ expr = ast.parse(annotation, mode='eval').body
707
+ except (SyntaxError, ValueError):
708
+ return None
709
+
710
+ if not isinstance(expr, ast.Subscript):
711
+ return None
712
+
713
+ base_name = _name_of(expr.value)
714
+ if base_name is None or base_name.split('.')[-1] not in {
715
+ 'Generator',
716
+ 'AsyncGenerator',
717
+ }:
718
+ return None
719
+
720
+ slice_node = expr.slice
721
+ if not isinstance(slice_node, ast.Tuple) or not slice_node.elts:
722
+ return None
723
+
724
+ first = slice_node.elts[0]
725
+ segment = ast.get_source_segment(annotation, first)
726
+ if segment is None:
727
+ segment = ast.unparse(first)
728
+
729
+ return segment.strip()
730
+
731
+
660
732
  def _detect_multiple_return_signatures(
661
733
  lines: list[str],
662
734
  start_idx: int,
@@ -697,6 +769,12 @@ def _rewrite_return_signature(line: str, annotation: str) -> str:
697
769
  colon_idx = stripped.find(':')
698
770
  if colon_idx != -1:
699
771
  name = stripped[:colon_idx].rstrip()
772
+ # Only treat the colon as a signature separator if something precedes
773
+ # it. rST cross references such as ``:class:`Foo``` start with a colon,
774
+ # in which case we just want to output the synced annotation.
775
+ if not name:
776
+ return f'{indent}{annotation}'
777
+
700
778
  return f'{indent}{name} : {annotation}'
701
779
 
702
780
  return f'{indent}{annotation}'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: format-docstring
3
- Version: 0.2.4
3
+ Version: 0.2.6
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>
@@ -60,6 +60,7 @@ tests/test_data/end_to_end/numpy/new_lines_before_and_after.txt
60
60
  tests/test_data/end_to_end/numpy/no_format_docstring_comment.txt
61
61
  tests/test_data/end_to_end/numpy/param_signature_without_type.txt
62
62
  tests/test_data/end_to_end/numpy/parameters_returns_raises_wrapping.txt
63
+ tests/test_data/end_to_end/numpy/rST_cross_reference.txt
63
64
  tests/test_data/end_to_end/numpy/returns_signature_and_description.txt
64
65
  tests/test_data/end_to_end/numpy/section_headings_with_colons.txt
65
66
  tests/test_data/end_to_end/numpy/section_title_fixed.txt
@@ -72,6 +73,7 @@ tests/test_data/end_to_end/numpy/signature_sync_returns.txt
72
73
  tests/test_data/end_to_end/numpy/signature_sync_yields.txt
73
74
  tests/test_data/end_to_end/numpy/single_line_docstring.txt
74
75
  tests/test_data/end_to_end/numpy/texts_are_rewrapped.txt
76
+ tests/test_data/end_to_end/numpy/variadic_signature_without_colon.txt
75
77
  tests/test_data/end_to_end/numpy/very_long_unbreakable_word.txt
76
78
  tests/test_data/integration_test/numpy/after.ipynb
77
79
  tests/test_data/integration_test/numpy/after.py
@@ -104,4 +106,5 @@ tests/test_data/line_wrap/numpy/section_title_fixed.txt
104
106
  tests/test_data/line_wrap/numpy/sections_notes_examples.txt
105
107
  tests/test_data/line_wrap/numpy/signature_line_is_not_wrapped.txt
106
108
  tests/test_data/line_wrap/numpy/texts_are_rewrapped.txt
109
+ tests/test_data/line_wrap/numpy/variadic_signature_without_colon.txt
107
110
  tests/test_data/line_wrap/numpy/very_long_unbreakable_word.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.4"
7
+ version = "0.2.6"
8
8
  dependencies = [
9
9
  "click>=8.0",
10
10
  "jupyter-notebook-parser>=0.1.4",
@@ -0,0 +1,72 @@
1
+ LINE_LENGTH: 79
2
+
3
+ **********
4
+
5
+ def myfunc() -> MyClass:
6
+ """
7
+ Do something
8
+
9
+ Returns
10
+ -------
11
+ :class:`MyClass`
12
+ An instance of MyClass
13
+ """
14
+ pass
15
+
16
+
17
+ def uses_cross_refs(
18
+ thing: MyClass,
19
+ creator: Callable[[MyClass], MyClass],
20
+ ) -> MyClass:
21
+ """
22
+ Uses parameters and returns that reference rST roles.
23
+
24
+ Parameters
25
+ ----------
26
+ thing : :class:`MyClass`
27
+ Object to transform.
28
+ creator : :meth:`MyClass.build`
29
+ Callback that builds a new instance.
30
+
31
+ Returns
32
+ -------
33
+ :meth:`MyClass.build`
34
+ A created instance.
35
+ """
36
+ return creator(thing)
37
+
38
+
39
+ **********
40
+
41
+ def myfunc() -> MyClass:
42
+ """
43
+ Do something
44
+
45
+ Returns
46
+ -------
47
+ MyClass
48
+ An instance of MyClass
49
+ """
50
+ pass
51
+
52
+
53
+ def uses_cross_refs(
54
+ thing: MyClass,
55
+ creator: Callable[[MyClass], MyClass],
56
+ ) -> MyClass:
57
+ """
58
+ Uses parameters and returns that reference rST roles.
59
+
60
+ Parameters
61
+ ----------
62
+ thing : MyClass
63
+ Object to transform.
64
+ creator : Callable[[MyClass], MyClass]
65
+ Callback that builds a new instance.
66
+
67
+ Returns
68
+ -------
69
+ MyClass
70
+ A created instance.
71
+ """
72
+ return creator(thing)
@@ -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
@@ -0,0 +1,58 @@
1
+ LINE_LENGTH: 75
2
+
3
+ **********
4
+ def function_with_variadics():
5
+ """
6
+ Parameters
7
+ ----------
8
+ arg1 : str
9
+ Something short
10
+ **kwargs
11
+ Keyword args that should remain on their own entry.
12
+ *custom_args
13
+ Positional extras that should wrap to their own block even without a colon.
14
+ **some_other_strange_name
15
+ Keyword extras that likewise need wrapping to stay with their entry.
16
+ *explicit_typed_args : tuple[int, ...]
17
+ Shows a variadic entry that still provides a type annotation.
18
+ **typed_kwargs : dict[str, int]
19
+ Another variadic keyword argument that uses a type hint.
20
+ """
21
+ return (
22
+ arg1,
23
+ kwargs,
24
+ custom_args,
25
+ some_other_strange_name,
26
+ explicit_typed_args,
27
+ typed_kwargs,
28
+ )
29
+
30
+ **********
31
+
32
+ def function_with_variadics():
33
+ """
34
+ Parameters
35
+ ----------
36
+ arg1 : str
37
+ Something short
38
+ **kwargs
39
+ Keyword args that should remain on their own entry.
40
+ *custom_args
41
+ Positional extras that should wrap to their own block even without
42
+ a colon.
43
+ **some_other_strange_name
44
+ Keyword extras that likewise need wrapping to stay with their
45
+ entry.
46
+ *explicit_typed_args : tuple[int, ...]
47
+ Shows a variadic entry that still provides a type annotation.
48
+ **typed_kwargs : dict[str, int]
49
+ Another variadic keyword argument that uses a type hint.
50
+ """
51
+ return (
52
+ arg1,
53
+ kwargs,
54
+ custom_args,
55
+ some_other_strange_name,
56
+ explicit_typed_args,
57
+ typed_kwargs,
58
+ )
@@ -0,0 +1,38 @@
1
+ LINE_LENGTH: 55
2
+
3
+ **********
4
+ Parameters
5
+ ----------
6
+ arg1 : str
7
+ Something short
8
+ **kwargs
9
+ Keyword args that should remain on their own entry.
10
+ *custom_args
11
+ Positional extras that should wrap to their own block even without a colon.
12
+ **some_other_strange_name
13
+ Keyword extras that likewise need wrapping to stay with their entry.
14
+ *explicit_typed_args : tuple[int, ...]
15
+ Shows a variadic entry that still provides a type annotation.
16
+ **typed_kwargs : dict[str, int]
17
+ Another variadic keyword argument that uses a type hint.
18
+
19
+ **********
20
+
21
+ Parameters
22
+ ----------
23
+ arg1 : str
24
+ Something short
25
+ **kwargs
26
+ Keyword args that should remain on their own entry.
27
+ *custom_args
28
+ Positional extras that should wrap to their own
29
+ block even without a colon.
30
+ **some_other_strange_name
31
+ Keyword extras that likewise need wrapping to stay
32
+ with their entry.
33
+ *explicit_typed_args : tuple[int, ...]
34
+ Shows a variadic entry that still provides a type
35
+ annotation.
36
+ **typed_kwargs : dict[str, int]
37
+ Another variadic keyword argument that uses a type
38
+ hint.
@@ -4,7 +4,6 @@ from textwrap import dedent
4
4
 
5
5
  import pytest
6
6
 
7
- import format_docstring.docstring_rewriter
8
7
  from format_docstring import docstring_rewriter
9
8
 
10
9
 
@@ -57,6 +56,57 @@ def test_rebuild_literal(literal: str, content: str, expected: str) -> None:
57
56
  assert docstring_rewriter.rebuild_literal(literal, content) == expected
58
57
 
59
58
 
59
+ @pytest.mark.parametrize(
60
+ ('segment', 'expected'),
61
+ [
62
+ (
63
+ dedent(
64
+ """
65
+ Literal[
66
+ 'auto', 'default', 'flex', 'scale', 'priority'
67
+ ]
68
+ | None
69
+ | NotGiven
70
+ """
71
+ ),
72
+ "Literal['auto', 'default', 'flex', 'scale', 'priority'] | None | NotGiven", # noqa: E501
73
+ ),
74
+ (
75
+ dedent(
76
+ """
77
+ Optional[
78
+ 'Widget'
79
+ ]
80
+ | None
81
+ """
82
+ ),
83
+ "Optional['Widget'] | None",
84
+ ),
85
+ (
86
+ dedent(
87
+ """
88
+ tuple[
89
+ dict[str, int],
90
+ list[
91
+ tuple[str, float]
92
+ ]
93
+ ]
94
+ """
95
+ ),
96
+ 'tuple[dict[str, int], list[tuple[str, float]]]',
97
+ ),
98
+ ],
99
+ )
100
+ def test_normalize_signature_segment_multiline_cases(
101
+ segment: str, expected: str
102
+ ) -> None:
103
+ """
104
+ Multiline annotations should normalize without inserting bracket gaps.
105
+ """
106
+ normalized = docstring_rewriter._normalize_signature_segment(segment) # noqa: SLF001
107
+ assert normalized == expected
108
+
109
+
60
110
  @pytest.mark.parametrize(
61
111
  ('src', 'selector', 'has_doc'),
62
112
  [
@@ -232,9 +282,7 @@ def test_wrap_docstring_numpy_parameters_and_examples() -> None:
232
282
  """ # noqa: E501
233
283
  ).strip('\n')
234
284
 
235
- wrapped = format_docstring.docstring_rewriter.wrap_docstring(
236
- doc, line_length=79
237
- )
285
+ wrapped = docstring_rewriter.wrap_docstring(doc, line_length=79)
238
286
 
239
287
  temp: str = 'very very very very very very very very very very'
240
288