format-docstring 0.2.5__tar.gz → 0.2.7__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 (114) hide show
  1. {format_docstring-0.2.5 → format_docstring-0.2.7}/CHANGELOG.md +20 -0
  2. {format_docstring-0.2.5 → format_docstring-0.2.7}/PKG-INFO +1 -1
  3. {format_docstring-0.2.5 → format_docstring-0.2.7}/format_docstring/docstring_rewriter.py +57 -6
  4. {format_docstring-0.2.5 → format_docstring-0.2.7}/format_docstring/line_wrap_numpy.py +39 -12
  5. {format_docstring-0.2.5 → format_docstring-0.2.7}/format_docstring.egg-info/PKG-INFO +1 -1
  6. {format_docstring-0.2.5 → format_docstring-0.2.7}/format_docstring.egg-info/SOURCES.txt +5 -0
  7. {format_docstring-0.2.5 → format_docstring-0.2.7}/pyproject.toml +1 -1
  8. format_docstring-0.2.7/tests/test_data/end_to_end/numpy/non_ascii_docstrings.txt +89 -0
  9. format_docstring-0.2.7/tests/test_data/end_to_end/numpy/rST_cross_reference.txt +72 -0
  10. format_docstring-0.2.7/tests/test_data/end_to_end/numpy/variadic_signature_without_colon.txt +58 -0
  11. format_docstring-0.2.7/tests/test_data/line_wrap/numpy/non_ascii_docstrings.txt +89 -0
  12. format_docstring-0.2.7/tests/test_data/line_wrap/numpy/variadic_signature_without_colon.txt +38 -0
  13. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_docstring_rewriter.py +61 -5
  14. {format_docstring-0.2.5 → format_docstring-0.2.7}/.github/workflows/python-package.yml +0 -0
  15. {format_docstring-0.2.5 → format_docstring-0.2.7}/.github/workflows/python-publish.yml +0 -0
  16. {format_docstring-0.2.5 → format_docstring-0.2.7}/.gitignore +0 -0
  17. {format_docstring-0.2.5 → format_docstring-0.2.7}/.pre-commit-config.yaml +0 -0
  18. {format_docstring-0.2.5 → format_docstring-0.2.7}/.pre-commit-hooks.yaml +0 -0
  19. {format_docstring-0.2.5 → format_docstring-0.2.7}/AGENTS.md +0 -0
  20. {format_docstring-0.2.5 → format_docstring-0.2.7}/LICENSE +0 -0
  21. {format_docstring-0.2.5 → format_docstring-0.2.7}/README.md +0 -0
  22. {format_docstring-0.2.5 → format_docstring-0.2.7}/format_docstring/__init__.py +0 -0
  23. {format_docstring-0.2.5 → format_docstring-0.2.7}/format_docstring/base_fixer.py +0 -0
  24. {format_docstring-0.2.5 → format_docstring-0.2.7}/format_docstring/config.py +0 -0
  25. {format_docstring-0.2.5 → format_docstring-0.2.7}/format_docstring/line_wrap_google.py +0 -0
  26. {format_docstring-0.2.5 → format_docstring-0.2.7}/format_docstring/line_wrap_utils.py +0 -0
  27. {format_docstring-0.2.5 → format_docstring-0.2.7}/format_docstring/main_jupyter.py +0 -0
  28. {format_docstring-0.2.5 → format_docstring-0.2.7}/format_docstring/main_py.py +0 -0
  29. {format_docstring-0.2.5 → format_docstring-0.2.7}/format_docstring.egg-info/dependency_links.txt +0 -0
  30. {format_docstring-0.2.5 → format_docstring-0.2.7}/format_docstring.egg-info/entry_points.txt +0 -0
  31. {format_docstring-0.2.5 → format_docstring-0.2.7}/format_docstring.egg-info/requires.txt +0 -0
  32. {format_docstring-0.2.5 → format_docstring-0.2.7}/format_docstring.egg-info/top_level.txt +0 -0
  33. {format_docstring-0.2.5 → format_docstring-0.2.7}/muff.toml +0 -0
  34. {format_docstring-0.2.5 → format_docstring-0.2.7}/requirements.dev +0 -0
  35. {format_docstring-0.2.5 → format_docstring-0.2.7}/setup.cfg +0 -0
  36. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/__init__.py +0 -0
  37. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/helpers.py +0 -0
  38. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_base_fixer.py +0 -0
  39. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_config.py +0 -0
  40. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/README.md +0 -0
  41. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/arg_name_is_default.txt +0 -0
  42. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/colon_spacing_fix.txt +0 -0
  43. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/contents_that_are_not_wrapped.txt +0 -0
  44. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/default_value_standardization.txt +0 -0
  45. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/empty_lines_are_respected.txt +0 -0
  46. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/examples_section.txt +0 -0
  47. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/existing_linebreaks_should_not_be_respected.txt +0 -0
  48. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/fix_rst_backticks.txt +0 -0
  49. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/four_level_nested_classes.txt +0 -0
  50. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/indent_four_levels_16_spaces_width_10.txt +0 -0
  51. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/indent_misaligned_all.txt +0 -0
  52. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/indent_two_levels_8_spaces.txt +0 -0
  53. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/line_length_2.txt +0 -0
  54. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/mismatched_underlines.txt +0 -0
  55. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/mismatched_underlines_one_dash.txt +0 -0
  56. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/mismatched_underlines_two_dashes.txt +0 -0
  57. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/module_level_docstring.txt +0 -0
  58. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/new_lines_before_and_after.txt +0 -0
  59. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/no_format_docstring_comment.txt +0 -0
  60. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/param_signature_without_type.txt +0 -0
  61. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/parameters_returns_raises_wrapping.txt +0 -0
  62. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/returns_signature_and_description.txt +0 -0
  63. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/section_headings_with_colons.txt +0 -0
  64. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/section_title_fixed.txt +0 -0
  65. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/sections_notes_examples.txt +0 -0
  66. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/signature_dont_sync_raises.txt +0 -0
  67. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/signature_line_is_not_wrapped.txt +0 -0
  68. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/signature_sync_class_docstrings.txt +0 -0
  69. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/signature_sync_parameters.txt +0 -0
  70. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/signature_sync_returns.txt +0 -0
  71. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/signature_sync_yields.txt +0 -0
  72. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/single_line_docstring.txt +0 -0
  73. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/texts_are_rewrapped.txt +0 -0
  74. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/end_to_end/numpy/very_long_unbreakable_word.txt +0 -0
  75. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/integration_test/numpy/after.ipynb +0 -0
  76. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/integration_test/numpy/after.py +0 -0
  77. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/integration_test/numpy/after_50.ipynb +0 -0
  78. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/integration_test/numpy/after_50.py +0 -0
  79. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/integration_test/numpy/before.ipynb +0 -0
  80. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/integration_test/numpy/before.py +0 -0
  81. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/jupyter/before.ipynb +0 -0
  82. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/jupyter/verbose_before.ipynb +0 -0
  83. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/README.md +0 -0
  84. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/colon_spacing_fix.txt +0 -0
  85. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/contents_that_are_not_wrapped.txt +0 -0
  86. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/default_value_standardization.txt +0 -0
  87. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/empty_lines_are_respected.txt +0 -0
  88. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/examples_section.txt +0 -0
  89. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/existing_linebreaks_should_not_be_respected.txt +0 -0
  90. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/fix_rst_backticks.txt +0 -0
  91. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/indent_four_levels_16_spaces_width_10.txt +0 -0
  92. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/indent_two_levels_8_spaces.txt +0 -0
  93. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/line_length_2.txt +0 -0
  94. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/mismatched_underlines.txt +0 -0
  95. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/mismatched_underlines_one_dash.txt +0 -0
  96. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/mismatched_underlines_two_dashes.txt +0 -0
  97. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/module_level_docstring.txt +0 -0
  98. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/param_signature_without_type.txt +0 -0
  99. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/parameters_returns_raises_wrapping.txt +0 -0
  100. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/returns_signature_and_description.txt +0 -0
  101. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/section_headings_with_colons.txt +0 -0
  102. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/section_title_fixed.txt +0 -0
  103. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/sections_notes_examples.txt +0 -0
  104. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/signature_line_is_not_wrapped.txt +0 -0
  105. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/texts_are_rewrapped.txt +0 -0
  106. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/line_wrap/numpy/very_long_unbreakable_word.txt +0 -0
  107. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_data/playground.py +0 -0
  108. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_line_wrap_google.py +0 -0
  109. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_line_wrap_numpy.py +0 -0
  110. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_line_wrap_utils.py +0 -0
  111. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_main_jupyter.py +0 -0
  112. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_main_py.py +0 -0
  113. {format_docstring-0.2.5 → format_docstring-0.2.7}/tests/test_playground.py +0 -0
  114. {format_docstring-0.2.5 → format_docstring-0.2.7}/tox.ini +0 -0
@@ -6,6 +6,26 @@ 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.7] - 2025-12-04
10
+
11
+ - Fixed
12
+ - Bug where non-ASCII characters would mess up auto formatting
13
+ - Full diff
14
+ - https://github.com/jsh9/format-docstring/compare/0.2.6...0.2.7
15
+
16
+ ## [0.2.6] - 2025-11-21
17
+
18
+ - Fixed
19
+ - Multiline annotations now normalize without inserting spaces inside
20
+ `Literal[...]` or other bracketed signatures, even when they span several
21
+ lines.
22
+ - A bug where bare `*args`/`**kwargs` signature lines (typed or untyped)
23
+ would be merged into previous descriptions
24
+ - NumPy signature syncing left the `:class:` / `:meth:` role prefixes behind,
25
+ producing mismatched annotations like ` : MyClass`
26
+ - Full diff
27
+ - https://github.com/jsh9/format-docstring/compare/0.2.5...0.2.6
28
+
9
29
  ## [0.2.5] - 2025-11-20
10
30
 
11
31
  - Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: format-docstring
3
- Version: 0.2.5
3
+ Version: 0.2.7
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
@@ -420,8 +427,23 @@ def build_replacement_docstring(
420
427
  if not hasattr(val, 'lineno') or not hasattr(val, 'end_lineno'):
421
428
  return None
422
429
 
423
- start: int = calc_abs_pos(line_starts, val.lineno, val.col_offset)
424
- end: int = calc_abs_pos(line_starts, val.end_lineno, val.end_col_offset) # type: ignore[arg-type]
430
+ # ``end_lineno``/``end_col_offset`` are optional on older AST nodes or
431
+ # when running under tooling that strips positional info, so bail out if
432
+ # they are missing to avoid slicing with ``None`` later.
433
+ end_lineno: int | None = getattr(val, 'end_lineno', None)
434
+ end_col_offset: int | None = getattr(val, 'end_col_offset', None)
435
+ if end_lineno is None or end_col_offset is None:
436
+ return None
437
+
438
+ start: int = calc_abs_pos(
439
+ source_code, line_starts, val.lineno, val.col_offset
440
+ )
441
+ end: int = calc_abs_pos(
442
+ source_code,
443
+ line_starts,
444
+ end_lineno,
445
+ end_col_offset,
446
+ )
425
447
  original_literal = source_code[start:end]
426
448
 
427
449
  if _has_inline_no_format_comment(source_code, end):
@@ -517,12 +539,19 @@ def find_docstring(node: ModuleClassOrFunc) -> ast.Expr | None:
517
539
  return None
518
540
 
519
541
 
520
- def calc_abs_pos(line_starts: list[int], lineno: int, col: int) -> int:
542
+ def calc_abs_pos(
543
+ source_code: str, line_starts: list[int], lineno: int, col: int
544
+ ) -> int:
521
545
  """
522
546
  Convert a (lineno, col) pair to an absolute index.
523
547
 
524
548
  Parameters
525
549
  ----------
550
+ source_code : str
551
+ Full source text for computing character offsets. AST column offsets
552
+ are byte-based, so we need the actual text to translate them back to
553
+ character indices when multi-byte Unicode code points (e.g., 😄, é, 文)
554
+ are present.
526
555
  line_starts : list[int]
527
556
  Precomputed start offsets for each line, from :func:`_line_starts`.
528
557
  lineno : int
@@ -535,7 +564,29 @@ def calc_abs_pos(line_starts: list[int], lineno: int, col: int) -> int:
535
564
  int
536
565
  The absolute character index into the source string.
537
566
  """
538
- return line_starts[lineno - 1] + col
567
+ line_idx = lineno - 1
568
+ line_start = line_starts[line_idx]
569
+ next_line_start = (
570
+ line_starts[line_idx + 1]
571
+ if line_idx + 1 < len(line_starts)
572
+ else len(source_code)
573
+ )
574
+ line_segment = source_code[line_start:next_line_start]
575
+
576
+ # Column offsets from the AST are measured in bytes, so convert them back
577
+ # to character offsets when slicing the original ``str`` source. Iterate
578
+ # through the current line until reaching the requested byte position.
579
+ byte_count = 0
580
+ char_offset = 0
581
+ for char in line_segment:
582
+ if byte_count >= col:
583
+ break
584
+
585
+ byte_count += len(char.encode('utf-8'))
586
+ char_offset += 1
587
+
588
+ # Clamp to the line length in case the reported byte offset overshoots.
589
+ return line_start + min(char_offset, len(line_segment))
539
590
 
540
591
 
541
592
  def rebuild_literal(original_literal: str, content: str) -> str | None:
@@ -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)
@@ -341,6 +345,13 @@ _PARAM_SIGNATURE_RE = re.compile(
341
345
  rf'^\s*\*{{0,2}}{START}{CONT}*(?:\s*,\s*\*{{0,2}}{START}{CONT}*)*\s*:\s*.*$'
342
346
  )
343
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
+
344
355
 
345
356
  def _is_param_signature(text: str) -> bool:
346
357
  r"""
@@ -371,6 +382,16 @@ def _is_param_signature(text: str) -> bool:
371
382
  return bool(_PARAM_SIGNATURE_RE.match(text))
372
383
 
373
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
+
374
395
  def _fix_colon_spacing(line: str) -> str:
375
396
  """
376
397
  Fix spacing around colons in parameter signature lines.
@@ -748,6 +769,12 @@ def _rewrite_return_signature(line: str, annotation: str) -> str:
748
769
  colon_idx = stripped.find(':')
749
770
  if colon_idx != -1:
750
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
+
751
778
  return f'{indent}{name} : {annotation}'
752
779
 
753
780
  return f'{indent}{annotation}'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: format-docstring
3
- Version: 0.2.5
3
+ Version: 0.2.7
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>
@@ -58,8 +58,10 @@ tests/test_data/end_to_end/numpy/mismatched_underlines_two_dashes.txt
58
58
  tests/test_data/end_to_end/numpy/module_level_docstring.txt
59
59
  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
+ tests/test_data/end_to_end/numpy/non_ascii_docstrings.txt
61
62
  tests/test_data/end_to_end/numpy/param_signature_without_type.txt
62
63
  tests/test_data/end_to_end/numpy/parameters_returns_raises_wrapping.txt
64
+ tests/test_data/end_to_end/numpy/rST_cross_reference.txt
63
65
  tests/test_data/end_to_end/numpy/returns_signature_and_description.txt
64
66
  tests/test_data/end_to_end/numpy/section_headings_with_colons.txt
65
67
  tests/test_data/end_to_end/numpy/section_title_fixed.txt
@@ -72,6 +74,7 @@ tests/test_data/end_to_end/numpy/signature_sync_returns.txt
72
74
  tests/test_data/end_to_end/numpy/signature_sync_yields.txt
73
75
  tests/test_data/end_to_end/numpy/single_line_docstring.txt
74
76
  tests/test_data/end_to_end/numpy/texts_are_rewrapped.txt
77
+ tests/test_data/end_to_end/numpy/variadic_signature_without_colon.txt
75
78
  tests/test_data/end_to_end/numpy/very_long_unbreakable_word.txt
76
79
  tests/test_data/integration_test/numpy/after.ipynb
77
80
  tests/test_data/integration_test/numpy/after.py
@@ -96,6 +99,7 @@ tests/test_data/line_wrap/numpy/mismatched_underlines.txt
96
99
  tests/test_data/line_wrap/numpy/mismatched_underlines_one_dash.txt
97
100
  tests/test_data/line_wrap/numpy/mismatched_underlines_two_dashes.txt
98
101
  tests/test_data/line_wrap/numpy/module_level_docstring.txt
102
+ tests/test_data/line_wrap/numpy/non_ascii_docstrings.txt
99
103
  tests/test_data/line_wrap/numpy/param_signature_without_type.txt
100
104
  tests/test_data/line_wrap/numpy/parameters_returns_raises_wrapping.txt
101
105
  tests/test_data/line_wrap/numpy/returns_signature_and_description.txt
@@ -104,4 +108,5 @@ tests/test_data/line_wrap/numpy/section_title_fixed.txt
104
108
  tests/test_data/line_wrap/numpy/sections_notes_examples.txt
105
109
  tests/test_data/line_wrap/numpy/signature_line_is_not_wrapped.txt
106
110
  tests/test_data/line_wrap/numpy/texts_are_rewrapped.txt
111
+ tests/test_data/line_wrap/numpy/variadic_signature_without_colon.txt
107
112
  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.5"
7
+ version = "0.2.7"
8
8
  dependencies = [
9
9
  "click>=8.0",
10
10
  "jupyter-notebook-parser>=0.1.4",
@@ -0,0 +1,89 @@
1
+ LINE_LENGTH: 79
2
+
3
+ **********
4
+
5
+ # Non-ASCII characters should not interfere with line length calculation.
6
+
7
+ def unicode_docstring_pythagoras() -> None:
8
+ """a² + b² = c²"""
9
+ return None
10
+
11
+ def unicode_docstring_quadratic() -> None:
12
+ """Δ = √(b² - 4ac)"""
13
+ return None
14
+
15
+ def unicode_docstring_emoji() -> None:
16
+ """emoji 😄🚀 with sparkle ✨"""
17
+ return None
18
+
19
+ def unicode_docstring_accents() -> None:
20
+ """accents naïve façade jalapeño año"""
21
+ return None
22
+
23
+ def unicode_docstring_greek() -> None:
24
+ """Greek letters αβγδεζηθ"""
25
+ return None
26
+
27
+ def unicode_docstring_cyrillic() -> None:
28
+ """Cyrillic Привет мир"""
29
+ return None
30
+
31
+ def unicode_docstring_cjk() -> None:
32
+ """CJK 面積は平方メートルです"""
33
+ return None
34
+
35
+ def unicode_docstring_pi_tau() -> None:
36
+ """Math π·r² + τ is about 6.283"""
37
+ return None
38
+
39
+ def unicode_docstring_currency() -> None:
40
+ """Currency mix € £ ¥ ₽"""
41
+ return None
42
+
43
+ def unicode_docstring_hebrew() -> None:
44
+ """Hebrew שלום עליכם"""
45
+ return None
46
+
47
+ **********
48
+
49
+ # Non-ASCII characters should not interfere with line length calculation.
50
+
51
+ def unicode_docstring_pythagoras() -> None:
52
+ """a² + b² = c²"""
53
+ return None
54
+
55
+ def unicode_docstring_quadratic() -> None:
56
+ """Δ = √(b² - 4ac)"""
57
+ return None
58
+
59
+ def unicode_docstring_emoji() -> None:
60
+ """emoji 😄🚀 with sparkle ✨"""
61
+ return None
62
+
63
+ def unicode_docstring_accents() -> None:
64
+ """accents naïve façade jalapeño año"""
65
+ return None
66
+
67
+ def unicode_docstring_greek() -> None:
68
+ """Greek letters αβγδεζηθ"""
69
+ return None
70
+
71
+ def unicode_docstring_cyrillic() -> None:
72
+ """Cyrillic Привет мир"""
73
+ return None
74
+
75
+ def unicode_docstring_cjk() -> None:
76
+ """CJK 面積は平方メートルです"""
77
+ return None
78
+
79
+ def unicode_docstring_pi_tau() -> None:
80
+ """Math π·r² + τ is about 6.283"""
81
+ return None
82
+
83
+ def unicode_docstring_currency() -> None:
84
+ """Currency mix € £ ¥ ₽"""
85
+ return None
86
+
87
+ def unicode_docstring_hebrew() -> None:
88
+ """Hebrew שלום עליכם"""
89
+ return None
@@ -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)
@@ -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,89 @@
1
+ LINE_LENGTH: 79
2
+
3
+ **********
4
+
5
+ Non-ASCII characters should not interfere with line length calculation.
6
+
7
+ Function f_square
8
+ -----------------
9
+ a² + b² = c²
10
+
11
+ Function f_delta
12
+ ----------------
13
+ Δ = √(b² - 4ac)
14
+
15
+ Function f_emoji
16
+ ----------------
17
+ emoji 😄🚀 with sparkle ✨
18
+
19
+ Function f_accents
20
+ ------------------
21
+ accents naïve façade jalapeño año
22
+
23
+ Function f_greek
24
+ ----------------
25
+ Greek letters αβγδεζηθ
26
+
27
+ Function f_cyrillic
28
+ -------------------
29
+ Cyrillic Привет мир
30
+
31
+ Function f_cjk
32
+ ---------------
33
+ CJK 面積は平方メートルです
34
+
35
+ Function f_pi_tau
36
+ -----------------
37
+ Math π·r² + τ is about 6.283
38
+
39
+ Function f_currency
40
+ -------------------
41
+ Currency mix € £ ¥ ₽
42
+
43
+ Function f_hebrew
44
+ -----------------
45
+ Hebrew שלום עליכם
46
+
47
+ **********
48
+
49
+ Non-ASCII characters should not interfere with line length calculation.
50
+
51
+ Function f_square
52
+ -----------------
53
+ a² + b² = c²
54
+
55
+ Function f_delta
56
+ ----------------
57
+ Δ = √(b² - 4ac)
58
+
59
+ Function f_emoji
60
+ ----------------
61
+ emoji 😄🚀 with sparkle ✨
62
+
63
+ Function f_accents
64
+ ------------------
65
+ accents naïve façade jalapeño año
66
+
67
+ Function f_greek
68
+ ----------------
69
+ Greek letters αβγδεζηθ
70
+
71
+ Function f_cyrillic
72
+ -------------------
73
+ Cyrillic Привет мир
74
+
75
+ Function f_cjk
76
+ ---------------
77
+ CJK 面積は平方メートルです
78
+
79
+ Function f_pi_tau
80
+ -----------------
81
+ Math π·r² + τ is about 6.283
82
+
83
+ Function f_currency
84
+ -------------------
85
+ Currency mix € £ ¥ ₽
86
+
87
+ Function f_hebrew
88
+ -----------------
89
+ Hebrew שלום עליכם
@@ -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
 
@@ -29,12 +28,20 @@ def test_calc_line_starts(src: str, expected: list[int]) -> None:
29
28
  ('a\n\nxyz\n', 2, 0, 2),
30
29
  ('a\n\nxyz\n', 3, 2, 5),
31
30
  ('one\ntwo\nthree', 2, 1, 5),
31
+ (
32
+ 'def f():\n """π"""\n pass\n',
33
+ 2,
34
+ len(' """π"""'.encode()),
35
+ len('def f():\n') + len(' """π"""'),
36
+ ),
32
37
  ],
33
38
  )
34
39
  def test_calc_abs_pos(src: str, lineno: int, col: int, expected: int) -> None:
35
40
  """Convert (lineno, col) to absolute indices using starts mapping."""
36
41
  starts = docstring_rewriter.calc_line_starts(src)
37
- assert docstring_rewriter.calc_abs_pos(starts, lineno, col) == expected
42
+ assert (
43
+ docstring_rewriter.calc_abs_pos(src, starts, lineno, col) == expected
44
+ )
38
45
 
39
46
 
40
47
  @pytest.mark.parametrize(
@@ -57,6 +64,57 @@ def test_rebuild_literal(literal: str, content: str, expected: str) -> None:
57
64
  assert docstring_rewriter.rebuild_literal(literal, content) == expected
58
65
 
59
66
 
67
+ @pytest.mark.parametrize(
68
+ ('segment', 'expected'),
69
+ [
70
+ (
71
+ dedent(
72
+ """
73
+ Literal[
74
+ 'auto', 'default', 'flex', 'scale', 'priority'
75
+ ]
76
+ | None
77
+ | NotGiven
78
+ """
79
+ ),
80
+ "Literal['auto', 'default', 'flex', 'scale', 'priority'] | None | NotGiven", # noqa: E501
81
+ ),
82
+ (
83
+ dedent(
84
+ """
85
+ Optional[
86
+ 'Widget'
87
+ ]
88
+ | None
89
+ """
90
+ ),
91
+ "Optional['Widget'] | None",
92
+ ),
93
+ (
94
+ dedent(
95
+ """
96
+ tuple[
97
+ dict[str, int],
98
+ list[
99
+ tuple[str, float]
100
+ ]
101
+ ]
102
+ """
103
+ ),
104
+ 'tuple[dict[str, int], list[tuple[str, float]]]',
105
+ ),
106
+ ],
107
+ )
108
+ def test_normalize_signature_segment_multiline_cases(
109
+ segment: str, expected: str
110
+ ) -> None:
111
+ """
112
+ Multiline annotations should normalize without inserting bracket gaps.
113
+ """
114
+ normalized = docstring_rewriter._normalize_signature_segment(segment) # noqa: SLF001
115
+ assert normalized == expected
116
+
117
+
60
118
  @pytest.mark.parametrize(
61
119
  ('src', 'selector', 'has_doc'),
62
120
  [
@@ -232,9 +290,7 @@ def test_wrap_docstring_numpy_parameters_and_examples() -> None:
232
290
  """ # noqa: E501
233
291
  ).strip('\n')
234
292
 
235
- wrapped = format_docstring.docstring_rewriter.wrap_docstring(
236
- doc, line_length=79
237
- )
293
+ wrapped = docstring_rewriter.wrap_docstring(doc, line_length=79)
238
294
 
239
295
  temp: str = 'very very very very very very very very very very'
240
296