format-docstring 0.1.0__py3-none-any.whl
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.
- format_docstring/__init__.py +5 -0
- format_docstring/base_fixer.py +70 -0
- format_docstring/config.py +211 -0
- format_docstring/docstring_rewriter.py +314 -0
- format_docstring/line_wrap_google.py +7 -0
- format_docstring/line_wrap_numpy.py +387 -0
- format_docstring/line_wrap_utils.py +781 -0
- format_docstring/main_jupyter.py +165 -0
- format_docstring/main_py.py +125 -0
- format_docstring-0.1.0.dist-info/METADATA +311 -0
- format_docstring-0.1.0.dist-info/RECORD +15 -0
- format_docstring-0.1.0.dist-info/WHEEL +5 -0
- format_docstring-0.1.0.dist-info/entry_points.txt +3 -0
- format_docstring-0.1.0.dist-info/licenses/LICENSE +21 -0
- format_docstring-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseFixer:
|
|
9
|
+
"""Base class for fixing code formatting issues."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
path: str,
|
|
14
|
+
exclude_pattern: str = r'\.git|\.tox|\.pytest_cache',
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Initialize the fixer with a path and optional exclude pattern."""
|
|
17
|
+
self.path = path
|
|
18
|
+
self.exclude_pattern = exclude_pattern
|
|
19
|
+
|
|
20
|
+
def _get_files_to_process(
|
|
21
|
+
self, directory: Path, pattern: str
|
|
22
|
+
) -> list[Path]:
|
|
23
|
+
"""Get list of files to process, filtered by exclude pattern."""
|
|
24
|
+
all_files = sorted(directory.rglob(pattern))
|
|
25
|
+
return [
|
|
26
|
+
f
|
|
27
|
+
for f in all_files
|
|
28
|
+
if not should_exclude_file(f, self.exclude_pattern)
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
def fix_one_directory_or_one_file(self) -> int:
|
|
32
|
+
"""
|
|
33
|
+
Fix formatting in a single file or all Python files in a directory.
|
|
34
|
+
"""
|
|
35
|
+
path_obj = Path(self.path)
|
|
36
|
+
|
|
37
|
+
if path_obj.is_file():
|
|
38
|
+
if should_exclude_file(path_obj, self.exclude_pattern):
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
return self.fix_one_file(path_obj.as_posix())
|
|
42
|
+
|
|
43
|
+
# Is a directory
|
|
44
|
+
filenames = self._get_files_to_process(path_obj, '*.py')
|
|
45
|
+
all_status = set()
|
|
46
|
+
for filename in filenames:
|
|
47
|
+
status = self.fix_one_file(filename.as_posix())
|
|
48
|
+
all_status.add(status)
|
|
49
|
+
|
|
50
|
+
return 0 if not all_status or all_status == {0} else 1
|
|
51
|
+
|
|
52
|
+
def fix_one_file(self, *varargs: Any, **kwargs: Any) -> int:
|
|
53
|
+
"""Fix formatting in a single file."""
|
|
54
|
+
raise NotImplementedError('Please implement this method')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def should_exclude_file(file_path: Path, exclude_pattern: str) -> bool:
|
|
58
|
+
"""Return True if `file_path` matches the provided exclude regex.
|
|
59
|
+
|
|
60
|
+
If `exclude_pattern` is empty or invalid, no files are excluded.
|
|
61
|
+
"""
|
|
62
|
+
if not exclude_pattern:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
exclude_regex = re.compile(exclude_pattern)
|
|
67
|
+
except re.error:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
return bool(exclude_regex.search(file_path.as_posix()))
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Configuration file parsing for format-docstring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
if sys.version_info >= (3, 11):
|
|
12
|
+
import tomllib
|
|
13
|
+
else:
|
|
14
|
+
import tomli as tomllib
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def find_config_file(paths: list[str] | tuple[str, ...] | None) -> Path | None:
|
|
18
|
+
"""
|
|
19
|
+
Find pyproject.toml by walking up from the target path(s).
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
paths : list[str] | tuple[str, ...] | None
|
|
24
|
+
The paths to search from. If None or empty, searches from cwd.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
Path | None
|
|
29
|
+
The path to pyproject.toml if found, None otherwise.
|
|
30
|
+
"""
|
|
31
|
+
if not paths:
|
|
32
|
+
search_path = Path.cwd()
|
|
33
|
+
elif len(paths) == 1:
|
|
34
|
+
search_path = Path(paths[0])
|
|
35
|
+
if search_path.is_file():
|
|
36
|
+
search_path = search_path.parent
|
|
37
|
+
else:
|
|
38
|
+
# Find common parent folder
|
|
39
|
+
search_path = _find_common_parent(paths)
|
|
40
|
+
|
|
41
|
+
# Walk up the directory tree looking for pyproject.toml
|
|
42
|
+
current = search_path.resolve()
|
|
43
|
+
while True:
|
|
44
|
+
config_file = current / 'pyproject.toml'
|
|
45
|
+
if config_file.exists():
|
|
46
|
+
return config_file
|
|
47
|
+
|
|
48
|
+
parent = current.parent
|
|
49
|
+
if parent == current: # Reached root
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
current = parent
|
|
53
|
+
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _find_common_parent(paths: list[str] | tuple[str, ...]) -> Path:
|
|
58
|
+
"""
|
|
59
|
+
Find the common parent folder of the given paths.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
paths : list[str] | tuple[str, ...]
|
|
64
|
+
The paths to find the common parent for.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
Path
|
|
69
|
+
The common parent folder.
|
|
70
|
+
"""
|
|
71
|
+
path_objs = [Path(p) for p in paths]
|
|
72
|
+
|
|
73
|
+
# For single path, return its parent if it looks like a file
|
|
74
|
+
if len(path_objs) == 1:
|
|
75
|
+
path = path_objs[0]
|
|
76
|
+
# If it has a file extension, treat as file
|
|
77
|
+
if path.suffix:
|
|
78
|
+
return path.parent
|
|
79
|
+
# If it exists and is a file, return parent
|
|
80
|
+
if path.exists() and path.is_file():
|
|
81
|
+
return path.parent
|
|
82
|
+
# Otherwise treat as directory
|
|
83
|
+
return path
|
|
84
|
+
|
|
85
|
+
# For multiple paths, find common parent by comparing parts
|
|
86
|
+
# Convert all file paths to their parent directories
|
|
87
|
+
dir_paths = []
|
|
88
|
+
for path in path_objs:
|
|
89
|
+
if path.suffix or (path.exists() and path.is_file()):
|
|
90
|
+
dir_paths.append(path.parent)
|
|
91
|
+
else:
|
|
92
|
+
dir_paths.append(path)
|
|
93
|
+
|
|
94
|
+
# Start with the first directory
|
|
95
|
+
common = dir_paths[0]
|
|
96
|
+
|
|
97
|
+
# Find common parent by comparing parts
|
|
98
|
+
for path in dir_paths[1:]:
|
|
99
|
+
# Find common parts
|
|
100
|
+
common_parts = []
|
|
101
|
+
for p1, p2 in zip(common.parts, path.parts, strict=False):
|
|
102
|
+
if p1 == p2:
|
|
103
|
+
common_parts.append(p1)
|
|
104
|
+
else:
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
if common_parts:
|
|
108
|
+
common = Path(*common_parts)
|
|
109
|
+
else:
|
|
110
|
+
# No common parent, use cwd
|
|
111
|
+
common = Path.cwd()
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
return common
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def load_config_from_file(config_file: Path) -> dict[str, Any]:
|
|
118
|
+
"""
|
|
119
|
+
Load configuration from a pyproject.toml file.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
config_file : Path
|
|
124
|
+
Path to the configuration file.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
dict[str, Any]
|
|
129
|
+
Configuration dictionary with normalized keys (underscores).
|
|
130
|
+
"""
|
|
131
|
+
if not config_file.exists():
|
|
132
|
+
return {}
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
with open(config_file, 'rb') as fp:
|
|
136
|
+
raw_config = tomllib.load(fp)
|
|
137
|
+
|
|
138
|
+
# Extract [tool.format_docstring] section
|
|
139
|
+
format_docstring_section = raw_config.get('tool', {}).get(
|
|
140
|
+
'format_docstring', {}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Normalize keys: replace hyphens with underscores
|
|
144
|
+
return {
|
|
145
|
+
k.replace('-', '_'): v for k, v in format_docstring_section.items()
|
|
146
|
+
}
|
|
147
|
+
except Exception:
|
|
148
|
+
# If there's any error reading/parsing the file, return empty config
|
|
149
|
+
return {}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def update_click_context(
|
|
153
|
+
ctx: click.Context,
|
|
154
|
+
config: dict[str, Any],
|
|
155
|
+
) -> None:
|
|
156
|
+
"""
|
|
157
|
+
Update the Click context's default_map with configuration values.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
ctx : click.Context
|
|
162
|
+
The Click context to update.
|
|
163
|
+
config : dict[str, Any]
|
|
164
|
+
Configuration dictionary to merge into the context.
|
|
165
|
+
"""
|
|
166
|
+
if ctx.default_map is None:
|
|
167
|
+
ctx.default_map = {}
|
|
168
|
+
|
|
169
|
+
ctx.default_map.update(config)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def inject_config_from_file(
|
|
173
|
+
ctx: click.Context,
|
|
174
|
+
param: click.Parameter, # noqa: ARG001 (required by Click callback signature)
|
|
175
|
+
value: str | None,
|
|
176
|
+
) -> str | None:
|
|
177
|
+
"""
|
|
178
|
+
Click callback to inject configuration from a config file.
|
|
179
|
+
|
|
180
|
+
This is used as a callback for the --config option.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
ctx : click.Context
|
|
185
|
+
The Click context.
|
|
186
|
+
param : click.Parameter
|
|
187
|
+
The Click parameter (unused, required by Click callback signature).
|
|
188
|
+
value : str | None
|
|
189
|
+
The path to the config file, or None to auto-discover.
|
|
190
|
+
|
|
191
|
+
Returns
|
|
192
|
+
-------
|
|
193
|
+
str | None
|
|
194
|
+
The config file path if found/specified, None otherwise.
|
|
195
|
+
"""
|
|
196
|
+
config_file: Path | None
|
|
197
|
+
|
|
198
|
+
if value:
|
|
199
|
+
# User specified a config file
|
|
200
|
+
config_file = Path(value)
|
|
201
|
+
else:
|
|
202
|
+
# Auto-discover config file from paths
|
|
203
|
+
paths = ctx.params.get('paths')
|
|
204
|
+
config_file = find_config_file(paths)
|
|
205
|
+
|
|
206
|
+
if config_file and config_file.exists():
|
|
207
|
+
config = load_config_from_file(config_file)
|
|
208
|
+
update_click_context(ctx, config)
|
|
209
|
+
return str(config_file)
|
|
210
|
+
|
|
211
|
+
return None
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from format_docstring.line_wrap_google import wrap_docstring_google
|
|
6
|
+
from format_docstring.line_wrap_numpy import (
|
|
7
|
+
handle_single_line_docstring_that_is_a_bit_too_long,
|
|
8
|
+
wrap_docstring_numpy,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
ModuleClassOrFunc = (
|
|
12
|
+
ast.Module | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def fix_src(
|
|
17
|
+
source_code: str,
|
|
18
|
+
*,
|
|
19
|
+
line_length: int = 79,
|
|
20
|
+
docstring_style: str = 'numpy',
|
|
21
|
+
) -> str:
|
|
22
|
+
"""Return code with only docstrings updated to wrapped content.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
source_code : str
|
|
27
|
+
The full Python source code to process.
|
|
28
|
+
line_length : int, default=79
|
|
29
|
+
Target maximum line length for wrapping logic.
|
|
30
|
+
|
|
31
|
+
docstring_style : str, default='numpy'
|
|
32
|
+
The docstring style to target ('numpy' or 'google').
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
str
|
|
37
|
+
The updated source code. Only docstring literals are changed; all
|
|
38
|
+
other formatting is preserved.
|
|
39
|
+
|
|
40
|
+
Notes
|
|
41
|
+
-----
|
|
42
|
+
This function avoids ``ast.unparse`` and instead replaces docstring
|
|
43
|
+
literal spans directly in the original text to preserve non-docstring
|
|
44
|
+
formatting and comments.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
tree: ast.Module = ast.parse(source_code)
|
|
48
|
+
line_starts: list[int] = calc_line_starts(source_code)
|
|
49
|
+
|
|
50
|
+
replacements: list[tuple[int, int, str]] = []
|
|
51
|
+
|
|
52
|
+
# Module-level docstring
|
|
53
|
+
rep = build_replacement_docstring(
|
|
54
|
+
tree, source_code, line_starts, line_length, docstring_style
|
|
55
|
+
)
|
|
56
|
+
if rep is not None:
|
|
57
|
+
replacements.append(rep)
|
|
58
|
+
|
|
59
|
+
# Class/function-level docstrings
|
|
60
|
+
for node in ast.walk(tree):
|
|
61
|
+
if isinstance(
|
|
62
|
+
node, ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef
|
|
63
|
+
):
|
|
64
|
+
rep = build_replacement_docstring(
|
|
65
|
+
node, source_code, line_starts, line_length, docstring_style
|
|
66
|
+
)
|
|
67
|
+
if rep is not None:
|
|
68
|
+
replacements.append(rep)
|
|
69
|
+
|
|
70
|
+
# Apply replacements from the end to avoid shifting offsets
|
|
71
|
+
if not replacements:
|
|
72
|
+
return source_code
|
|
73
|
+
|
|
74
|
+
replacements.sort(key=lambda x: x[0], reverse=True)
|
|
75
|
+
new_src = source_code
|
|
76
|
+
for start, end, text in replacements:
|
|
77
|
+
new_src = new_src[:start] + text + new_src[end:]
|
|
78
|
+
|
|
79
|
+
return new_src
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def calc_line_starts(source_code: str) -> list[int]:
|
|
83
|
+
"""Return starting offsets for each line in the source string.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
source_code : str
|
|
88
|
+
The source text to analyze.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
list[int]
|
|
93
|
+
A list of absolute indices for the start of each line.
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
starts: list[int] = [0]
|
|
97
|
+
for i, ch in enumerate(source_code):
|
|
98
|
+
if ch == '\n':
|
|
99
|
+
starts.append(i + 1)
|
|
100
|
+
|
|
101
|
+
return starts
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def build_replacement_docstring(
|
|
105
|
+
node: ModuleClassOrFunc,
|
|
106
|
+
source_code: str,
|
|
107
|
+
line_starts: list[int],
|
|
108
|
+
line_length: int,
|
|
109
|
+
docstring_style: str = 'numpy',
|
|
110
|
+
) -> tuple[int, int, str] | None:
|
|
111
|
+
"""Compute a single docstring replacement for the given node.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
node : ModuleClassOrFunc
|
|
116
|
+
The AST node owning the docstring.
|
|
117
|
+
source_code : str
|
|
118
|
+
The original source text.
|
|
119
|
+
line_starts : list[int]
|
|
120
|
+
Line start offsets from :func:`_line_starts`.
|
|
121
|
+
line_length : int
|
|
122
|
+
Target maximum line length for wrapping logic.
|
|
123
|
+
|
|
124
|
+
docstring_style : str, default='numpy'
|
|
125
|
+
The docstring style to target ('numpy' or 'google').
|
|
126
|
+
|
|
127
|
+
Returns
|
|
128
|
+
-------
|
|
129
|
+
tuple[int, int, str] or None
|
|
130
|
+
A tuple ``(start, end, new_literal)`` indicating the replacement
|
|
131
|
+
range and text, or ``None`` if no change is needed.
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
docstring_obj: ast.Expr | None = find_docstring(node)
|
|
135
|
+
if docstring_obj is None:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
val: ast.Constant = docstring_obj.value # type: ignore[assignment]
|
|
139
|
+
if not hasattr(val, 'lineno') or not hasattr(val, 'end_lineno'):
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
start: int = calc_abs_pos(line_starts, val.lineno, val.col_offset)
|
|
143
|
+
end: int = calc_abs_pos(line_starts, val.end_lineno, val.end_col_offset) # type: ignore[arg-type] # noqa: LN002
|
|
144
|
+
original_literal = source_code[start:end]
|
|
145
|
+
|
|
146
|
+
doc: str | None = ast.get_docstring(node, clean=False)
|
|
147
|
+
if doc is None:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
# Use the docstring literal's column offset as the indentation level for
|
|
151
|
+
# formatting. This lets the wrapper ensure leading/trailing newlines plus
|
|
152
|
+
# matching spaces are present so closing quotes align with the parent's
|
|
153
|
+
# indentation.
|
|
154
|
+
leading_indent: int = getattr(val, 'col_offset', 0)
|
|
155
|
+
|
|
156
|
+
# Only enforce leading/trailing newline+indent for multi-line docstrings
|
|
157
|
+
# or when wrapping will occur. Keep short single-line docstrings unchanged.
|
|
158
|
+
leading_indent_: int | None = (
|
|
159
|
+
leading_indent if ('\n' in doc or len(doc) > line_length) else None
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
wrapped: str = wrap_docstring(
|
|
163
|
+
doc,
|
|
164
|
+
line_length=line_length,
|
|
165
|
+
docstring_style=docstring_style,
|
|
166
|
+
leading_indent=leading_indent_, # type: ignore[arg-type]
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
new_literal: str | None = rebuild_literal(original_literal, wrapped)
|
|
170
|
+
|
|
171
|
+
new_literal = handle_single_line_docstring_that_is_a_bit_too_long(
|
|
172
|
+
whole_docstring_literal=new_literal,
|
|
173
|
+
docstring_content=wrapped,
|
|
174
|
+
docstring_starting_col=val.col_offset,
|
|
175
|
+
docstring_ending_col=val.end_col_offset, # type: ignore[arg-type]
|
|
176
|
+
line_length=line_length,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if new_literal is None or new_literal == original_literal:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
return start, end, new_literal
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def find_docstring(node: ModuleClassOrFunc) -> ast.Expr | None:
|
|
186
|
+
"""Return the first statement if it is a string-literal docstring.
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
node : ModuleClassOrFunc
|
|
191
|
+
An ``ast.Module``, ``ast.ClassDef``, ``ast.FunctionDef``, or
|
|
192
|
+
``ast.AsyncFunctionDef`` node.
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
ast.Expr or None
|
|
197
|
+
The ``ast.Expr`` node that holds the docstring literal, if present;
|
|
198
|
+
otherwise ``None``.
|
|
199
|
+
|
|
200
|
+
"""
|
|
201
|
+
body: list[ast.stmt] | None = getattr(node, 'body', None)
|
|
202
|
+
if not body:
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
first = body[0]
|
|
206
|
+
if not isinstance(first, ast.Expr):
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
val = first.value
|
|
210
|
+
if isinstance(val, ast.Constant) and isinstance(val.value, str):
|
|
211
|
+
return first
|
|
212
|
+
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def calc_abs_pos(line_starts: list[int], lineno: int, col: int) -> int:
|
|
217
|
+
"""Convert a (lineno, col) pair to an absolute index.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
line_starts : list[int]
|
|
222
|
+
Precomputed start offsets for each line, from :func:`_line_starts`.
|
|
223
|
+
lineno : int
|
|
224
|
+
1-based line number.
|
|
225
|
+
col : int
|
|
226
|
+
0-based column offset.
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
int
|
|
231
|
+
The absolute character index into the source string.
|
|
232
|
+
|
|
233
|
+
"""
|
|
234
|
+
return line_starts[lineno - 1] + col
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def rebuild_literal(original_literal: str, content: str) -> str | None:
|
|
238
|
+
"""Rebuild a string literal preserving prefix and quote style.
|
|
239
|
+
|
|
240
|
+
Parameters
|
|
241
|
+
----------
|
|
242
|
+
original_literal : str
|
|
243
|
+
The exact text of the original string literal including any prefix
|
|
244
|
+
and surrounding quotes.
|
|
245
|
+
content : str
|
|
246
|
+
The new inner content (without surrounding quotes).
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
str or None
|
|
251
|
+
A new literal string with the same prefix and quotes and the new
|
|
252
|
+
content. Returns ``None`` if the original cannot be parsed.
|
|
253
|
+
|
|
254
|
+
"""
|
|
255
|
+
i = 0
|
|
256
|
+
n = len(original_literal)
|
|
257
|
+
while i < n and original_literal[i] in 'rRuUbBfF':
|
|
258
|
+
i += 1
|
|
259
|
+
|
|
260
|
+
prefix = original_literal[:i]
|
|
261
|
+
|
|
262
|
+
delim = ''
|
|
263
|
+
if original_literal[i : i + 3] in ('"""', "'''"):
|
|
264
|
+
delim = original_literal[i : i + 3]
|
|
265
|
+
i += 3
|
|
266
|
+
elif i < n and original_literal[i] in ('"', "'"):
|
|
267
|
+
delim = original_literal[i]
|
|
268
|
+
i += 1
|
|
269
|
+
else:
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
return f'{prefix}{delim}{content}{delim}'
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def wrap_docstring(
|
|
276
|
+
docstring: str,
|
|
277
|
+
line_length: int = 79,
|
|
278
|
+
docstring_style: str = 'numpy',
|
|
279
|
+
leading_indent: int = 0,
|
|
280
|
+
) -> str:
|
|
281
|
+
"""Wrap a docstring to the given line length (stub).
|
|
282
|
+
|
|
283
|
+
Parameters
|
|
284
|
+
----------
|
|
285
|
+
docstring : str
|
|
286
|
+
The original docstring contents without quotes.
|
|
287
|
+
line_length : int, default=79
|
|
288
|
+
Target maximum line length for wrapping logic.
|
|
289
|
+
docstring_style : str, default="numpy"
|
|
290
|
+
The docstring style to target ('numpy' or 'google').
|
|
291
|
+
leading_indent : int, default=0
|
|
292
|
+
The number of indentation spaces of this docstring.
|
|
293
|
+
|
|
294
|
+
Returns
|
|
295
|
+
-------
|
|
296
|
+
str
|
|
297
|
+
The transformed docstring contents.
|
|
298
|
+
|
|
299
|
+
Notes
|
|
300
|
+
-----
|
|
301
|
+
This function dispatches to style-specific implementations:
|
|
302
|
+
- 'numpy' -> wrap_docstring_numpy
|
|
303
|
+
- 'google' -> wrap_docstring_google
|
|
304
|
+
|
|
305
|
+
"""
|
|
306
|
+
style = (docstring_style or '').strip().lower()
|
|
307
|
+
if style == 'google':
|
|
308
|
+
return wrap_docstring_google(
|
|
309
|
+
docstring, line_length, leading_indent=leading_indent
|
|
310
|
+
)
|
|
311
|
+
# Default to NumPy-style for unknown/unspecified styles to be permissive.
|
|
312
|
+
return wrap_docstring_numpy(
|
|
313
|
+
docstring, line_length, leading_indent=leading_indent
|
|
314
|
+
)
|