gyomu-docstring 0.2.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.
- gyomu_docstring/__init__.py +0 -0
- gyomu_docstring/update/__init__.py +0 -0
- gyomu_docstring/update/apply_file_update.py +21 -0
- gyomu_docstring/update/apply_merge.py +407 -0
- gyomu_docstring/update/build_file_update.py +286 -0
- gyomu_docstring/update/docstring/__init__.py +0 -0
- gyomu_docstring/update/docstring/file_update_plan.py +16 -0
- gyomu_docstring/update/docstring/line.py +22 -0
- gyomu_docstring/update/docstring/merge_plan.py +109 -0
- gyomu_docstring/update/docstring/rendered_symbol.py +11 -0
- gyomu_docstring/update/docstring/updated_docstring.py +10 -0
- gyomu_docstring/update/internal/__init__.py +0 -0
- gyomu_docstring/update/internal/render_line.py +159 -0
- gyomu_docstring/update/internal/render_string.py +63 -0
- gyomu_docstring/update/render_docstring.py +40 -0
- gyomu_docstring-0.2.0.dist-info/METADATA +14 -0
- gyomu_docstring-0.2.0.dist-info/RECORD +18 -0
- gyomu_docstring-0.2.0.dist-info/WHEEL +4 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from gyomu_docstring.update.docstring.file_update_plan import FileUpdatePlan
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def apply_file_update_plan(
|
|
5
|
+
source: str,
|
|
6
|
+
plan: FileUpdatePlan,
|
|
7
|
+
) -> str:
|
|
8
|
+
result = source
|
|
9
|
+
|
|
10
|
+
for item in sorted(
|
|
11
|
+
plan.items,
|
|
12
|
+
key=lambda item: item.location.start_offset,
|
|
13
|
+
reverse=True,
|
|
14
|
+
):
|
|
15
|
+
result = (
|
|
16
|
+
result[: item.location.start_offset]
|
|
17
|
+
+ item.new_text
|
|
18
|
+
+ result[item.location.end_offset :]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return result
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
from gyomu_python_analysis.error.update import UpdateError
|
|
2
|
+
from gyomu_schema.schemas.python.docstring import (
|
|
3
|
+
DocstringAnalysis,
|
|
4
|
+
DocstringParametersSection,
|
|
5
|
+
DocstringParametersSectionItem,
|
|
6
|
+
DocstringRaisesSection,
|
|
7
|
+
DocstringRaisesSectionItem,
|
|
8
|
+
DocstringReturnsSection,
|
|
9
|
+
DocstringReturnsSectionItem,
|
|
10
|
+
DocstringSection,
|
|
11
|
+
DocstringStyle,
|
|
12
|
+
)
|
|
13
|
+
from gyomu_schema.schemas.python.file_analysis import FileAnalysisContext
|
|
14
|
+
from gyomu_schema.schemas.python.location import SourceLocation
|
|
15
|
+
from gyomu_schema.schemas.python.symbol_base import DeclarationKind
|
|
16
|
+
from gyomu_schema.schemas.python.types import DeclarationIdentity, PythonPath
|
|
17
|
+
from returns.result import Failure, Result, Success
|
|
18
|
+
|
|
19
|
+
from gyomu_docstring.update.docstring.merge_plan import (
|
|
20
|
+
DeleteAction,
|
|
21
|
+
MergeAction,
|
|
22
|
+
MergePlan,
|
|
23
|
+
ParamMergePlan,
|
|
24
|
+
PreserveAction,
|
|
25
|
+
RaiseMergePlan,
|
|
26
|
+
ReplaceAction,
|
|
27
|
+
ReturnActionValue,
|
|
28
|
+
)
|
|
29
|
+
from gyomu_docstring.update.docstring.updated_docstring import UpdatedDocstring
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def apply_merge_plans(
|
|
33
|
+
context: FileAnalysisContext, plans: tuple[MergePlan, ...]
|
|
34
|
+
) -> Result[tuple[UpdatedDocstring, ...], UpdateError]:
|
|
35
|
+
results: list[UpdatedDocstring] = []
|
|
36
|
+
|
|
37
|
+
for plan in plans:
|
|
38
|
+
result = apply_merge_plan(context, plan)
|
|
39
|
+
|
|
40
|
+
if isinstance(result, Failure):
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
results.append(result.unwrap())
|
|
44
|
+
|
|
45
|
+
return Success(tuple(results))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def apply_merge_plan(
|
|
49
|
+
context: FileAnalysisContext, plan: MergePlan
|
|
50
|
+
) -> Result[UpdatedDocstring, UpdateError]:
|
|
51
|
+
existing_docstring = context.metadata.parsed_docstring.get(plan.identity)
|
|
52
|
+
existing_symbol_or_method = context.metadata.symbols.get(plan.identity)
|
|
53
|
+
|
|
54
|
+
if existing_symbol_or_method is None:
|
|
55
|
+
return Failure(
|
|
56
|
+
UpdateError(
|
|
57
|
+
"Symbol/Method not found",
|
|
58
|
+
file_path=context.analysis.module_name,
|
|
59
|
+
phase="apply-merge",
|
|
60
|
+
identity=plan.identity,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
if (
|
|
64
|
+
existing_symbol_or_method.location is None
|
|
65
|
+
or existing_symbol_or_method.indent is None
|
|
66
|
+
):
|
|
67
|
+
return Failure(
|
|
68
|
+
UpdateError(
|
|
69
|
+
"Constructor private variable should not be here",
|
|
70
|
+
file_path=context.analysis.module_name,
|
|
71
|
+
phase="apply-merge",
|
|
72
|
+
identity=plan.identity,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
summary = _merge_summary(plan.summary, existing_docstring)
|
|
77
|
+
description = _merge_description(plan.description, existing_docstring)
|
|
78
|
+
|
|
79
|
+
arguments_result = _merge_arguments(
|
|
80
|
+
file_path=context.analysis.module_name,
|
|
81
|
+
identity=plan.identity,
|
|
82
|
+
plans=plan.params,
|
|
83
|
+
existing_docstring=existing_docstring,
|
|
84
|
+
)
|
|
85
|
+
if isinstance(arguments_result, Failure):
|
|
86
|
+
return arguments_result
|
|
87
|
+
|
|
88
|
+
returns = _merge_returns(
|
|
89
|
+
plan.returns,
|
|
90
|
+
existing_docstring,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
raises_result = _merge_raises(
|
|
94
|
+
file_path=context.analysis.module_name,
|
|
95
|
+
identity=plan.identity,
|
|
96
|
+
plans=plan.raises,
|
|
97
|
+
existing_docstring=existing_docstring,
|
|
98
|
+
)
|
|
99
|
+
if isinstance(raises_result, Failure):
|
|
100
|
+
return raises_result
|
|
101
|
+
|
|
102
|
+
sections = _merge_sections(
|
|
103
|
+
existing_docstring=existing_docstring,
|
|
104
|
+
arguments=arguments_result.unwrap(),
|
|
105
|
+
returns=returns,
|
|
106
|
+
raises=raises_result.unwrap(),
|
|
107
|
+
)
|
|
108
|
+
new_location = (
|
|
109
|
+
existing_docstring.location if existing_docstring is not None else None
|
|
110
|
+
)
|
|
111
|
+
if new_location is None:
|
|
112
|
+
new_location = SourceLocation(
|
|
113
|
+
start_line=existing_symbol_or_method.location.start_line,
|
|
114
|
+
end_line=existing_symbol_or_method.location.end_line,
|
|
115
|
+
start_offset=existing_symbol_or_method.location.end_offset,
|
|
116
|
+
end_offset=existing_symbol_or_method.location.end_offset,
|
|
117
|
+
start_column=existing_symbol_or_method.location.start_column,
|
|
118
|
+
end_column=existing_symbol_or_method.location.start_column,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
new_indent = existing_docstring.indent if existing_docstring is not None else None
|
|
122
|
+
|
|
123
|
+
if new_indent is None:
|
|
124
|
+
if existing_symbol_or_method.kind in (
|
|
125
|
+
DeclarationKind.CLASS,
|
|
126
|
+
DeclarationKind.FUNCTION,
|
|
127
|
+
DeclarationKind.METHOD,
|
|
128
|
+
):
|
|
129
|
+
new_indent = existing_symbol_or_method.indent + 4
|
|
130
|
+
else:
|
|
131
|
+
new_indent = existing_symbol_or_method.indent
|
|
132
|
+
|
|
133
|
+
updated_docstring = DocstringAnalysis(
|
|
134
|
+
raw=existing_docstring.raw if existing_docstring is not None else "",
|
|
135
|
+
summary=summary,
|
|
136
|
+
description=description,
|
|
137
|
+
style=(
|
|
138
|
+
existing_docstring.style
|
|
139
|
+
if existing_docstring is not None
|
|
140
|
+
else DocstringStyle.GOOGLE
|
|
141
|
+
),
|
|
142
|
+
location=new_location,
|
|
143
|
+
sections=sections,
|
|
144
|
+
indent=new_indent,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return Success(
|
|
148
|
+
UpdatedDocstring(
|
|
149
|
+
identity=plan.identity,
|
|
150
|
+
docstring=updated_docstring,
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _merge_sections(
|
|
156
|
+
existing_docstring: DocstringAnalysis | None,
|
|
157
|
+
arguments: tuple[DocstringParametersSectionItem, ...],
|
|
158
|
+
returns: DocstringReturnsSectionItem | None,
|
|
159
|
+
raises: tuple[DocstringRaisesSectionItem, ...],
|
|
160
|
+
) -> tuple[DocstringSection, ...]:
|
|
161
|
+
existing_sections = (
|
|
162
|
+
existing_docstring.sections if existing_docstring is not None else ()
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
sections: list[DocstringSection] = []
|
|
166
|
+
|
|
167
|
+
arguments_added = False
|
|
168
|
+
returns_added = False
|
|
169
|
+
raises_added = False
|
|
170
|
+
|
|
171
|
+
for section in existing_sections:
|
|
172
|
+
match section:
|
|
173
|
+
case DocstringParametersSection():
|
|
174
|
+
if arguments:
|
|
175
|
+
sections.append(DocstringParametersSection(items=arguments))
|
|
176
|
+
arguments_added = True
|
|
177
|
+
|
|
178
|
+
case DocstringReturnsSection():
|
|
179
|
+
if returns is not None:
|
|
180
|
+
sections.append(DocstringReturnsSection(item=returns))
|
|
181
|
+
returns_added = True
|
|
182
|
+
|
|
183
|
+
case DocstringRaisesSection():
|
|
184
|
+
if raises:
|
|
185
|
+
sections.append(DocstringRaisesSection(items=raises))
|
|
186
|
+
raises_added = True
|
|
187
|
+
|
|
188
|
+
case _:
|
|
189
|
+
sections.append(section)
|
|
190
|
+
|
|
191
|
+
if arguments and not arguments_added:
|
|
192
|
+
sections.append(DocstringParametersSection(items=arguments))
|
|
193
|
+
|
|
194
|
+
if returns is not None and not returns_added:
|
|
195
|
+
sections.append(DocstringReturnsSection(item=returns))
|
|
196
|
+
|
|
197
|
+
if raises and not raises_added:
|
|
198
|
+
sections.append(DocstringRaisesSection(items=raises))
|
|
199
|
+
|
|
200
|
+
return tuple(sections)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _merge_summary(
|
|
204
|
+
plan: MergeAction[str], existing_docstring: DocstringAnalysis | None
|
|
205
|
+
) -> str | None:
|
|
206
|
+
match plan.type:
|
|
207
|
+
case "preserve":
|
|
208
|
+
if existing_docstring is None:
|
|
209
|
+
return None
|
|
210
|
+
return existing_docstring.summary
|
|
211
|
+
case "delete":
|
|
212
|
+
return None
|
|
213
|
+
case "replace":
|
|
214
|
+
return plan.value
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _merge_description(
|
|
218
|
+
plan: MergeAction[str], existing_docstring: DocstringAnalysis | None
|
|
219
|
+
) -> str | None:
|
|
220
|
+
match plan.type:
|
|
221
|
+
case "preserve":
|
|
222
|
+
if existing_docstring is None:
|
|
223
|
+
return None
|
|
224
|
+
return existing_docstring.description
|
|
225
|
+
case "delete":
|
|
226
|
+
return None
|
|
227
|
+
case "replace":
|
|
228
|
+
return plan.value
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _find_section[T: DocstringSection](
|
|
232
|
+
existing_docstring: DocstringAnalysis | None, section_type: type[T]
|
|
233
|
+
) -> T | None:
|
|
234
|
+
if existing_docstring is None:
|
|
235
|
+
return None
|
|
236
|
+
existing_parameters = next(
|
|
237
|
+
(
|
|
238
|
+
section
|
|
239
|
+
for section in existing_docstring.sections
|
|
240
|
+
if isinstance(section, section_type)
|
|
241
|
+
),
|
|
242
|
+
None,
|
|
243
|
+
)
|
|
244
|
+
return existing_parameters
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _merge_arguments(
|
|
248
|
+
file_path: PythonPath,
|
|
249
|
+
identity: DeclarationIdentity,
|
|
250
|
+
plans: tuple[ParamMergePlan, ...],
|
|
251
|
+
existing_docstring: DocstringAnalysis | None,
|
|
252
|
+
) -> Result[
|
|
253
|
+
tuple[DocstringParametersSectionItem, ...],
|
|
254
|
+
UpdateError,
|
|
255
|
+
]:
|
|
256
|
+
parameters_section = _find_section(existing_docstring, DocstringParametersSection)
|
|
257
|
+
existing_parameters_by_name = (
|
|
258
|
+
{item.name: item for item in parameters_section.items}
|
|
259
|
+
if parameters_section is not None
|
|
260
|
+
else {}
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
merged: list[tuple[int, DocstringParametersSectionItem]] = []
|
|
264
|
+
|
|
265
|
+
for plan in plans:
|
|
266
|
+
existing_param = existing_parameters_by_name.get(plan.name)
|
|
267
|
+
|
|
268
|
+
match plan.action:
|
|
269
|
+
case PreserveAction():
|
|
270
|
+
if existing_param is not None:
|
|
271
|
+
merged.append((plan.sort_order, existing_param))
|
|
272
|
+
|
|
273
|
+
case DeleteAction():
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
case ReplaceAction(value=value):
|
|
277
|
+
if (
|
|
278
|
+
existing_param is None
|
|
279
|
+
and value.parameter_type is None
|
|
280
|
+
and value.description is None
|
|
281
|
+
):
|
|
282
|
+
return Failure(
|
|
283
|
+
UpdateError(
|
|
284
|
+
file_path=file_path,
|
|
285
|
+
message="Planned Argument doesn not have valid Argument",
|
|
286
|
+
phase="update-plan",
|
|
287
|
+
details={"plannedParameter": plan},
|
|
288
|
+
identity=identity,
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
merged.append(
|
|
293
|
+
(
|
|
294
|
+
plan.sort_order,
|
|
295
|
+
DocstringParametersSectionItem(
|
|
296
|
+
name=plan.name,
|
|
297
|
+
type=(
|
|
298
|
+
value.parameter_type
|
|
299
|
+
if value.parameter_type is not None
|
|
300
|
+
else existing_param.type
|
|
301
|
+
if existing_param is not None
|
|
302
|
+
else None
|
|
303
|
+
),
|
|
304
|
+
description=(
|
|
305
|
+
value.description
|
|
306
|
+
if value.description is not None
|
|
307
|
+
else existing_param.description
|
|
308
|
+
if existing_param is not None
|
|
309
|
+
else ""
|
|
310
|
+
),
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
merged.sort(key=lambda item: item[0])
|
|
316
|
+
|
|
317
|
+
return Success(tuple(item for _, item in merged))
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _merge_returns(
|
|
321
|
+
plan: MergeAction[ReturnActionValue], existing_docstring: DocstringAnalysis | None
|
|
322
|
+
) -> DocstringReturnsSectionItem | None:
|
|
323
|
+
returns_section = _find_section(existing_docstring, DocstringReturnsSection)
|
|
324
|
+
existing_item = returns_section.item if returns_section is not None else None
|
|
325
|
+
match plan.type:
|
|
326
|
+
case "preserve":
|
|
327
|
+
if existing_item is None:
|
|
328
|
+
return None
|
|
329
|
+
return existing_item
|
|
330
|
+
case "delete":
|
|
331
|
+
return None
|
|
332
|
+
case "replace":
|
|
333
|
+
new_value = plan.value
|
|
334
|
+
return (
|
|
335
|
+
DocstringReturnsSectionItem(
|
|
336
|
+
description=new_value.description,
|
|
337
|
+
type=new_value.return_type
|
|
338
|
+
if new_value.return_type is not None
|
|
339
|
+
else existing_item.type
|
|
340
|
+
if existing_item is not None
|
|
341
|
+
else None,
|
|
342
|
+
)
|
|
343
|
+
if new_value.description is not None
|
|
344
|
+
else existing_item
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _merge_raises(
|
|
349
|
+
file_path: PythonPath,
|
|
350
|
+
identity: DeclarationIdentity,
|
|
351
|
+
plans: tuple[RaiseMergePlan, ...],
|
|
352
|
+
existing_docstring: DocstringAnalysis | None,
|
|
353
|
+
) -> Result[
|
|
354
|
+
tuple[DocstringRaisesSectionItem, ...],
|
|
355
|
+
UpdateError,
|
|
356
|
+
]:
|
|
357
|
+
raises_section = _find_section(existing_docstring, DocstringRaisesSection)
|
|
358
|
+
existing_raises_by_type = (
|
|
359
|
+
{item.type: item for item in raises_section.items}
|
|
360
|
+
if raises_section is not None
|
|
361
|
+
else {}
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
merged: list[tuple[int, DocstringRaisesSectionItem]] = []
|
|
365
|
+
|
|
366
|
+
for plan in plans:
|
|
367
|
+
existing_raise = existing_raises_by_type.get(plan.exception_type)
|
|
368
|
+
|
|
369
|
+
match plan.action:
|
|
370
|
+
case PreserveAction():
|
|
371
|
+
if existing_raise is not None:
|
|
372
|
+
merged.append((plan.sort_order, existing_raise))
|
|
373
|
+
|
|
374
|
+
case DeleteAction():
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
case ReplaceAction(value=value):
|
|
378
|
+
if existing_raise is None and value.description is None:
|
|
379
|
+
return Failure(
|
|
380
|
+
UpdateError(
|
|
381
|
+
file_path=file_path,
|
|
382
|
+
message="Planned Raises doesn not have valid Raise Item",
|
|
383
|
+
phase="update-plan",
|
|
384
|
+
details={"plannedParameter": plan},
|
|
385
|
+
identity=identity,
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
merged.append(
|
|
390
|
+
(
|
|
391
|
+
plan.sort_order,
|
|
392
|
+
DocstringRaisesSectionItem(
|
|
393
|
+
type=(value.exception_type),
|
|
394
|
+
description=(
|
|
395
|
+
value.description
|
|
396
|
+
if value.description is not None
|
|
397
|
+
else existing_raise.description
|
|
398
|
+
if existing_raise is not None
|
|
399
|
+
else ""
|
|
400
|
+
),
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
merged.sort(key=lambda item: item[0])
|
|
406
|
+
|
|
407
|
+
return Success(tuple(item for _, item in merged))
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import tokenize
|
|
3
|
+
|
|
4
|
+
from gyomu_python_analysis.error.update import UpdateError
|
|
5
|
+
from gyomu_schema.error.validation import ValidationError
|
|
6
|
+
from gyomu_schema.schemas.python.class_analysis import ClassAnalysis, InnerClassAnalysis
|
|
7
|
+
from gyomu_schema.schemas.python.file_analysis import FileAnalysisContext
|
|
8
|
+
from gyomu_schema.schemas.python.function_analysis import FunctionAnalysis
|
|
9
|
+
from gyomu_schema.schemas.python.method_analysis import MethodAnalysis
|
|
10
|
+
from gyomu_schema.schemas.python.symbol import MemberAnalysis, SymbolAnalysis
|
|
11
|
+
from returns.result import Failure, Result, Success
|
|
12
|
+
|
|
13
|
+
from gyomu_docstring.update.docstring.file_update_plan import (
|
|
14
|
+
FileUpdatePlan,
|
|
15
|
+
FileUpdatePlanEntry,
|
|
16
|
+
)
|
|
17
|
+
from gyomu_docstring.update.docstring.rendered_symbol import (
|
|
18
|
+
RenderedSymbolDocstring,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_file_update_plan(
|
|
23
|
+
context: FileAnalysisContext,
|
|
24
|
+
rendered_docstrings: tuple[RenderedSymbolDocstring, ...],
|
|
25
|
+
source: str,
|
|
26
|
+
) -> Result[FileUpdatePlan, UpdateError]:
|
|
27
|
+
entries_result = _build_file_update_plan_entries(
|
|
28
|
+
context=context,
|
|
29
|
+
rendered_docstrings=rendered_docstrings,
|
|
30
|
+
source=source,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if isinstance(entries_result, Failure):
|
|
34
|
+
return entries_result.alt(
|
|
35
|
+
UpdateError(
|
|
36
|
+
"fail to build file update plan entry",
|
|
37
|
+
file_path=context.analysis.module_name,
|
|
38
|
+
phase="update-plan",
|
|
39
|
+
identity=None,
|
|
40
|
+
).chain
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
entries = entries_result.unwrap()
|
|
44
|
+
|
|
45
|
+
validation_result = validate_file_update_plan_entries(entries)
|
|
46
|
+
|
|
47
|
+
if isinstance(validation_result, Failure):
|
|
48
|
+
return validation_result.alt(
|
|
49
|
+
UpdateError(
|
|
50
|
+
"fail to validate file update plan entry",
|
|
51
|
+
file_path=context.analysis.module_name,
|
|
52
|
+
phase="update-plan",
|
|
53
|
+
identity=None,
|
|
54
|
+
).chain
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return Success(FileUpdatePlan(items=entries))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _build_file_update_plan_entries(
|
|
61
|
+
context: FileAnalysisContext,
|
|
62
|
+
rendered_docstrings: tuple[RenderedSymbolDocstring, ...],
|
|
63
|
+
source: str,
|
|
64
|
+
) -> Result[tuple[FileUpdatePlanEntry, ...], ValidationError]:
|
|
65
|
+
entries: list[FileUpdatePlanEntry] = []
|
|
66
|
+
|
|
67
|
+
for rendered in rendered_docstrings:
|
|
68
|
+
result = build_file_update_plan_entry(
|
|
69
|
+
source=source,
|
|
70
|
+
context=context,
|
|
71
|
+
rendered=rendered,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if isinstance(result, Failure):
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
entries.append(result.unwrap())
|
|
78
|
+
|
|
79
|
+
return Success(tuple(entries))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def validate_file_update_plan_entries(
|
|
83
|
+
entries: tuple[FileUpdatePlanEntry, ...],
|
|
84
|
+
) -> Result[None, ValidationError]:
|
|
85
|
+
sorted_entries = sorted(
|
|
86
|
+
entries,
|
|
87
|
+
key=lambda entry: entry.location.start_offset,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
for entry in sorted_entries:
|
|
91
|
+
location = entry.location
|
|
92
|
+
|
|
93
|
+
if location.start_offset > location.end_offset:
|
|
94
|
+
return Failure(
|
|
95
|
+
ValidationError(
|
|
96
|
+
f"Invalid file update range for "
|
|
97
|
+
f"{entry.identity}: "
|
|
98
|
+
f"start_offset ({location.start_offset}) is greater than "
|
|
99
|
+
f"end_offset ({location.end_offset}).",
|
|
100
|
+
context="gyomu_docstring.update.build_file_update.validate_file_update_plan_entries",
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
for previous, current in zip(sorted_entries, sorted_entries[1:], strict=False):
|
|
105
|
+
previous_end = previous.location.end_offset
|
|
106
|
+
current_start = current.location.start_offset
|
|
107
|
+
|
|
108
|
+
if current_start < previous_end:
|
|
109
|
+
return Failure(
|
|
110
|
+
ValidationError(
|
|
111
|
+
f"Overlapping file update ranges: "
|
|
112
|
+
f"{previous.identity} "
|
|
113
|
+
f"[{previous.location.start_offset}, {previous_end}) and "
|
|
114
|
+
f"{current.identity} "
|
|
115
|
+
f"[{current_start}, {current.location.end_offset}).",
|
|
116
|
+
context="gyomu_docstring.update.build_file_update.validate_file_update_plan_entries",
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return Success(None)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def build_file_update_plan_entry(
|
|
124
|
+
source: str,
|
|
125
|
+
context: FileAnalysisContext,
|
|
126
|
+
rendered: RenderedSymbolDocstring,
|
|
127
|
+
) -> Result[FileUpdatePlanEntry, ValidationError]:
|
|
128
|
+
analysis = context.metadata.symbols.get(rendered.identity)
|
|
129
|
+
|
|
130
|
+
if analysis is None:
|
|
131
|
+
return Failure(
|
|
132
|
+
ValidationError(
|
|
133
|
+
message="Declaration Item Not Found",
|
|
134
|
+
context="gyomu_docstring.update.build_file_update.build_file_update_plan_entry",
|
|
135
|
+
details={"identity": rendered.identity},
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if analysis.location is None or analysis.indent is None:
|
|
140
|
+
return Failure(
|
|
141
|
+
ValidationError(
|
|
142
|
+
message="Declation Item is constructor parameter",
|
|
143
|
+
context="gyomu_docstring.update.build_file_update.build_file_update_plan_entry",
|
|
144
|
+
details={"identity": rendered.identity},
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if rendered.location.start_offset == rendered.location.end_offset:
|
|
149
|
+
return build_addition_entry(
|
|
150
|
+
source=source,
|
|
151
|
+
analysis=analysis,
|
|
152
|
+
rendered=rendered,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if rendered.docstring == "" or rendered.docstring is None:
|
|
156
|
+
return Success(
|
|
157
|
+
build_deletion_entry(
|
|
158
|
+
source=source,
|
|
159
|
+
rendered=rendered,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return Success(build_replacement_entry(rendered))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def build_addition_entry(
|
|
167
|
+
source: str,
|
|
168
|
+
analysis: SymbolAnalysis | MemberAnalysis,
|
|
169
|
+
rendered: RenderedSymbolDocstring,
|
|
170
|
+
) -> Result[FileUpdatePlanEntry, ValidationError]:
|
|
171
|
+
assert rendered.docstring
|
|
172
|
+
assert analysis.location
|
|
173
|
+
|
|
174
|
+
# TODO: 追加するdocstringはシンボルの直前ではなく、
|
|
175
|
+
# 定義ヘッダの直後に挿入する位置を計算する。
|
|
176
|
+
location = rendered.location
|
|
177
|
+
new_location = location.model_copy()
|
|
178
|
+
new_location.start_offset = location.start_offset
|
|
179
|
+
|
|
180
|
+
new_text = "\n" + rendered.docstring
|
|
181
|
+
if isinstance(
|
|
182
|
+
analysis, ClassAnalysis | FunctionAnalysis | InnerClassAnalysis | MethodAnalysis
|
|
183
|
+
):
|
|
184
|
+
end_result = _get_declaration_definition_end_offset(source, analysis)
|
|
185
|
+
if isinstance(end_result, Failure):
|
|
186
|
+
return end_result
|
|
187
|
+
new_location.start_offset = end_result.unwrap()
|
|
188
|
+
new_location.end_offset = new_location.start_offset
|
|
189
|
+
return Success(
|
|
190
|
+
FileUpdatePlanEntry(
|
|
191
|
+
identity=rendered.identity, location=new_location, new_text=new_text
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _get_declaration_definition_end_offset(
|
|
197
|
+
source: str,
|
|
198
|
+
analysis: ClassAnalysis | FunctionAnalysis | InnerClassAnalysis | MethodAnalysis,
|
|
199
|
+
) -> Result[int, ValidationError]:
|
|
200
|
+
assert analysis.location
|
|
201
|
+
|
|
202
|
+
declaration_source = source[
|
|
203
|
+
analysis.location.start_offset : analysis.location.end_offset
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
depth = 0
|
|
207
|
+
|
|
208
|
+
tokens = tokenize.generate_tokens(io.StringIO(declaration_source).readline)
|
|
209
|
+
|
|
210
|
+
for token in tokens:
|
|
211
|
+
if token.type != tokenize.OP:
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
if token.string in ("(", "[", "{"):
|
|
215
|
+
depth += 1
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
if token.string in (")", "]", "}"):
|
|
219
|
+
depth -= 1
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
if token.string == ":" and depth == 0:
|
|
223
|
+
relative_offset = _get_line_offset(
|
|
224
|
+
declaration_source,
|
|
225
|
+
token.end,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# ':' の直後から改行までの空白・タブを飛ばす
|
|
229
|
+
while (
|
|
230
|
+
relative_offset < len(declaration_source)
|
|
231
|
+
and declaration_source[relative_offset] not in "\r\n"
|
|
232
|
+
):
|
|
233
|
+
relative_offset += 1
|
|
234
|
+
|
|
235
|
+
return Success(analysis.location.start_offset + relative_offset)
|
|
236
|
+
|
|
237
|
+
return Failure(
|
|
238
|
+
ValidationError(
|
|
239
|
+
message="Declaration definition end not found",
|
|
240
|
+
context="gyomu_docstring.update.build_file_update._get_declaration_definition_end_offset",
|
|
241
|
+
details={"identity": analysis.identity},
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _get_line_offset(
|
|
247
|
+
source: str,
|
|
248
|
+
position: tuple[int, int],
|
|
249
|
+
) -> int:
|
|
250
|
+
row, column = position
|
|
251
|
+
|
|
252
|
+
lines = source.splitlines(keepends=True)
|
|
253
|
+
|
|
254
|
+
return sum(len(line) for line in lines[: row - 1]) + column
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def build_deletion_entry(
|
|
258
|
+
source: str, rendered: RenderedSymbolDocstring
|
|
259
|
+
) -> FileUpdatePlanEntry:
|
|
260
|
+
location = rendered.location.model_copy()
|
|
261
|
+
|
|
262
|
+
start = location.start_offset
|
|
263
|
+
while start > 0 and source[start - 1] in " \t":
|
|
264
|
+
start -= 1
|
|
265
|
+
|
|
266
|
+
end = location.end_offset
|
|
267
|
+
while end < len(source) and source[end] in " \t":
|
|
268
|
+
end += 1
|
|
269
|
+
|
|
270
|
+
location.start_offset = start
|
|
271
|
+
location.end_offset = end
|
|
272
|
+
|
|
273
|
+
return FileUpdatePlanEntry(
|
|
274
|
+
identity=rendered.identity,
|
|
275
|
+
location=location,
|
|
276
|
+
new_text="",
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def build_replacement_entry(rendered: RenderedSymbolDocstring) -> FileUpdatePlanEntry:
|
|
281
|
+
assert rendered.docstring
|
|
282
|
+
return FileUpdatePlanEntry(
|
|
283
|
+
identity=rendered.identity,
|
|
284
|
+
location=rendered.location,
|
|
285
|
+
new_text=rendered.docstring,
|
|
286
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from gyomu_schema.schemas.python.location import SourceLocation
|
|
4
|
+
from gyomu_schema.schemas.python.types import DeclarationIdentity
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class FileUpdatePlanEntry:
|
|
9
|
+
identity: DeclarationIdentity
|
|
10
|
+
location: SourceLocation
|
|
11
|
+
new_text: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class FileUpdatePlan:
|
|
16
|
+
items: tuple[FileUpdatePlanEntry, ...]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class DocstringText:
|
|
7
|
+
text: str
|
|
8
|
+
type: Literal["text"] = "text"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class DocstringBlank:
|
|
13
|
+
type: Literal["blank"] = "blank"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class DocstringSectionItem:
|
|
18
|
+
text: str
|
|
19
|
+
type: Literal["section"] = "section"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
type DocstringLine = DocstringText | DocstringSectionItem | DocstringBlank
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from gyomu_schema.schemas.python.types import DeclarationIdentity
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ReplaceAction[T]:
|
|
11
|
+
value: T
|
|
12
|
+
type: Literal["replace"] = "replace"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class DeleteAction:
|
|
17
|
+
type: Literal["delete"] = "delete"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class PreserveAction:
|
|
22
|
+
type: Literal["preserve"] = "preserve"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
type MergeAction[T] = ReplaceAction[T] | DeleteAction | PreserveAction
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConflictType(StrEnum):
|
|
29
|
+
HUMAN_EDITED = "human-edited"
|
|
30
|
+
MISSING_IN_NEW = "missing-in-new"
|
|
31
|
+
STRUCTURAL_MISMATCH = "structural-mismatch"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# 一時的な定義(後でLLM側に)
|
|
35
|
+
class ParamActionValue(BaseModel):
|
|
36
|
+
parameter_type: str | None = Field(description="Type hint of parameter")
|
|
37
|
+
description: str | None = Field(
|
|
38
|
+
description=(
|
|
39
|
+
"Complete replacement parameter metadata. "
|
|
40
|
+
"When using replace, provide the final parameter "
|
|
41
|
+
"documentation to be written."
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# 一時的な定義(後でLLM側に)
|
|
47
|
+
class ReturnActionValue(BaseModel):
|
|
48
|
+
return_type: str | None = Field(description="Type hint of return")
|
|
49
|
+
description: str | None = Field(
|
|
50
|
+
description=(
|
|
51
|
+
"Complete replacement parameter metadata. "
|
|
52
|
+
"When using replace, provide the final return "
|
|
53
|
+
"documentation to be written."
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# 一時的な定義(後でLLM側に)
|
|
59
|
+
class RaiseActionValue(BaseModel):
|
|
60
|
+
exception_type: str = Field(description="Type hint of exception")
|
|
61
|
+
description: str | None = Field(
|
|
62
|
+
description=(
|
|
63
|
+
"Complete replacement parameter metadata. "
|
|
64
|
+
"When using replace, provide the final raise "
|
|
65
|
+
"documentation to be written."
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class RaiseMergePlan:
|
|
72
|
+
exception_type: str
|
|
73
|
+
sort_order: int
|
|
74
|
+
action: MergeAction[RaiseActionValue]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class ParamMergePlan:
|
|
79
|
+
name: str
|
|
80
|
+
sort_order: int
|
|
81
|
+
action: MergeAction[ParamActionValue]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class MergeConflict:
|
|
86
|
+
symbol: str
|
|
87
|
+
type: ConflictType
|
|
88
|
+
message: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class MergePlan:
|
|
93
|
+
identity: DeclarationIdentity
|
|
94
|
+
|
|
95
|
+
summary: MergeAction[str]
|
|
96
|
+
|
|
97
|
+
description: MergeAction[str]
|
|
98
|
+
|
|
99
|
+
params: tuple[ParamMergePlan, ...]
|
|
100
|
+
|
|
101
|
+
returns: MergeAction[ReturnActionValue]
|
|
102
|
+
|
|
103
|
+
raises: tuple[RaiseMergePlan, ...]
|
|
104
|
+
|
|
105
|
+
conflicts: tuple[MergeConflict, ...]
|
|
106
|
+
|
|
107
|
+
confidence: float
|
|
108
|
+
|
|
109
|
+
average_confidence: float
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from gyomu_schema.schemas.python.location import SourceLocation
|
|
4
|
+
from gyomu_schema.schemas.python.types import DeclarationIdentity
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class RenderedSymbolDocstring:
|
|
9
|
+
identity: DeclarationIdentity
|
|
10
|
+
docstring: str | None
|
|
11
|
+
location: SourceLocation
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from gyomu_schema.schemas.python.docstring import DocstringAnalysis
|
|
4
|
+
from gyomu_schema.schemas.python.types import DeclarationIdentity
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class UpdatedDocstring:
|
|
9
|
+
identity: DeclarationIdentity
|
|
10
|
+
docstring: DocstringAnalysis
|
|
File without changes
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from gyomu_schema.schemas.python.docstring import (
|
|
2
|
+
DocstringCustomSection,
|
|
3
|
+
DocstringExamplesSection,
|
|
4
|
+
DocstringGyomuContextSection,
|
|
5
|
+
DocstringNotesSection,
|
|
6
|
+
DocstringParametersSection,
|
|
7
|
+
DocstringParametersSectionItem,
|
|
8
|
+
DocstringRaisesSection,
|
|
9
|
+
DocstringRaisesSectionItem,
|
|
10
|
+
DocstringReturnsSection,
|
|
11
|
+
DocstringSectionKind,
|
|
12
|
+
DocstringStyle,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from gyomu_docstring.update.docstring.line import (
|
|
16
|
+
DocstringBlank,
|
|
17
|
+
DocstringLine,
|
|
18
|
+
DocstringSectionItem,
|
|
19
|
+
DocstringText,
|
|
20
|
+
)
|
|
21
|
+
from gyomu_docstring.update.docstring.updated_docstring import UpdatedDocstring
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def render_docstring_lines(updated: UpdatedDocstring) -> tuple[DocstringLine, ...]:
|
|
25
|
+
lines: list[DocstringLine] = []
|
|
26
|
+
docstring = updated.docstring
|
|
27
|
+
if docstring.summary is not None:
|
|
28
|
+
lines.append(DocstringText(text=docstring.summary))
|
|
29
|
+
|
|
30
|
+
if docstring.description is not None:
|
|
31
|
+
lines.append(DocstringBlank())
|
|
32
|
+
lines.append(DocstringText(text=docstring.description))
|
|
33
|
+
|
|
34
|
+
for section in docstring.sections:
|
|
35
|
+
lines.append(DocstringBlank())
|
|
36
|
+
match section.kind:
|
|
37
|
+
case DocstringSectionKind.ARGS:
|
|
38
|
+
compute_args_tag(section, lines, docstring.style)
|
|
39
|
+
case DocstringSectionKind.RETURNS:
|
|
40
|
+
compute_returns_tag(section, lines, docstring.style)
|
|
41
|
+
case DocstringSectionKind.RAISES:
|
|
42
|
+
compute_raises_tag(section, lines, docstring.style)
|
|
43
|
+
case DocstringSectionKind.NOTES:
|
|
44
|
+
compute_notes_tag(section, lines, docstring.style)
|
|
45
|
+
case DocstringSectionKind.EXAMPLES:
|
|
46
|
+
compute_examples_tag(section, lines, docstring.style)
|
|
47
|
+
case DocstringSectionKind.GYOMU_CONTEXT:
|
|
48
|
+
compute_gyomu_context(section, lines, docstring.style)
|
|
49
|
+
case DocstringSectionKind.CUSTOM:
|
|
50
|
+
computeCustom_tag(section, lines, docstring.style)
|
|
51
|
+
return tuple(lines)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def computeCustom_tag(
|
|
55
|
+
section: DocstringCustomSection,
|
|
56
|
+
lines: list[DocstringLine],
|
|
57
|
+
style: DocstringStyle,
|
|
58
|
+
) -> None:
|
|
59
|
+
match style:
|
|
60
|
+
case DocstringStyle.GOOGLE:
|
|
61
|
+
lines.append(DocstringSectionItem(text=section.title + ":"))
|
|
62
|
+
lines.append(DocstringText(f" {section.value}"))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def compute_gyomu_context(
|
|
66
|
+
section: DocstringGyomuContextSection,
|
|
67
|
+
lines: list[DocstringLine],
|
|
68
|
+
style: DocstringStyle,
|
|
69
|
+
) -> None:
|
|
70
|
+
match style:
|
|
71
|
+
case DocstringStyle.GOOGLE:
|
|
72
|
+
lines.append(DocstringSectionItem(text="Gyomu Context:"))
|
|
73
|
+
lines.append(DocstringText(f" {section.value}"))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def compute_examples_tag(
|
|
77
|
+
section: DocstringExamplesSection, lines: list[DocstringLine], style: DocstringStyle
|
|
78
|
+
) -> None:
|
|
79
|
+
|
|
80
|
+
match style:
|
|
81
|
+
case DocstringStyle.GOOGLE:
|
|
82
|
+
lines.append(DocstringSectionItem(text="Examples:"))
|
|
83
|
+
for item in section.items:
|
|
84
|
+
lines.append(DocstringText(f" {item.value}"))
|
|
85
|
+
lines.append(DocstringBlank())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def compute_notes_tag(
|
|
89
|
+
section: DocstringNotesSection, lines: list[DocstringLine], style: DocstringStyle
|
|
90
|
+
) -> None:
|
|
91
|
+
match style:
|
|
92
|
+
case DocstringStyle.GOOGLE:
|
|
93
|
+
lines.append(DocstringSectionItem(text="Notes:"))
|
|
94
|
+
lines.append(DocstringText(f" {section.value}"))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def compute_raises_tag(
|
|
98
|
+
section: DocstringRaisesSection, lines: list[DocstringLine], style: DocstringStyle
|
|
99
|
+
) -> None:
|
|
100
|
+
if len(section.items) == 0:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
lines.append(DocstringSectionItem(text=_get_raises_section_name(style)))
|
|
104
|
+
for item in section.items:
|
|
105
|
+
lines.append(DocstringText(_compute_raises_item(item, style)))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _get_raises_section_name(style: DocstringStyle) -> str:
|
|
109
|
+
match style:
|
|
110
|
+
case DocstringStyle.GOOGLE:
|
|
111
|
+
return "Raises:"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _compute_raises_item(
|
|
115
|
+
item: DocstringRaisesSectionItem, style: DocstringStyle
|
|
116
|
+
) -> str:
|
|
117
|
+
match style:
|
|
118
|
+
case DocstringStyle.GOOGLE:
|
|
119
|
+
raise_type = f"{item.type}: " if item.type else ""
|
|
120
|
+
return f" {raise_type}{item.description}"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def compute_returns_tag(
|
|
124
|
+
section: DocstringReturnsSection, lines: list[DocstringLine], style: DocstringStyle
|
|
125
|
+
) -> None:
|
|
126
|
+
item = section.item
|
|
127
|
+
match style:
|
|
128
|
+
case DocstringStyle.GOOGLE:
|
|
129
|
+
lines.append(DocstringSectionItem(text="Returns:"))
|
|
130
|
+
return_type = f"{item.type}: " if item.type else ""
|
|
131
|
+
lines.append(DocstringText(f" {return_type}{item.description}"))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def compute_args_tag(
|
|
135
|
+
section: DocstringParametersSection,
|
|
136
|
+
lines: list[DocstringLine],
|
|
137
|
+
style: DocstringStyle,
|
|
138
|
+
) -> None:
|
|
139
|
+
if len(section.items) == 0:
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
lines.append(DocstringSectionItem(text=_get_args_section_name(style)))
|
|
143
|
+
for parameter in section.items:
|
|
144
|
+
lines.append(DocstringText(_compute_args_item(parameter, style)))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _get_args_section_name(style: DocstringStyle) -> str:
|
|
148
|
+
match style:
|
|
149
|
+
case DocstringStyle.GOOGLE:
|
|
150
|
+
return "Args:"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _compute_args_item(
|
|
154
|
+
parameter: DocstringParametersSectionItem, style: DocstringStyle
|
|
155
|
+
) -> str:
|
|
156
|
+
match style:
|
|
157
|
+
case DocstringStyle.GOOGLE:
|
|
158
|
+
param_type = f" ({parameter.type})" if parameter.type else ""
|
|
159
|
+
return f" {parameter.name}{param_type}: {parameter.description}"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from gyomu_docstring.update.docstring.line import (
|
|
2
|
+
DocstringBlank,
|
|
3
|
+
DocstringLine,
|
|
4
|
+
DocstringSectionItem,
|
|
5
|
+
DocstringText,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def render_docstring_string(
|
|
10
|
+
lines: tuple[DocstringLine, ...],
|
|
11
|
+
is_added: bool,
|
|
12
|
+
indent: int,
|
|
13
|
+
) -> str | None:
|
|
14
|
+
if not lines:
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
prefix = " " * indent
|
|
18
|
+
|
|
19
|
+
if _is_single_line_docstring(lines):
|
|
20
|
+
assert lines[0].type == "text"
|
|
21
|
+
text = lines[0].text
|
|
22
|
+
result = f'{prefix}"""{text}"""'
|
|
23
|
+
else:
|
|
24
|
+
first_line = compute_docstring_line(lines[0], prefix)
|
|
25
|
+
start = f'{prefix}"""{first_line.removeprefix(prefix)}'
|
|
26
|
+
|
|
27
|
+
string_lines = [
|
|
28
|
+
start,
|
|
29
|
+
*(compute_docstring_line(line, prefix) for line in lines[1:]),
|
|
30
|
+
f'{prefix}"""',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
result = "\n".join(string_lines)
|
|
34
|
+
|
|
35
|
+
if is_added:
|
|
36
|
+
result += "\n"
|
|
37
|
+
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def compute_docstring_line(
|
|
42
|
+
line: DocstringLine,
|
|
43
|
+
prefix: str,
|
|
44
|
+
) -> str:
|
|
45
|
+
match line:
|
|
46
|
+
case DocstringBlank():
|
|
47
|
+
return prefix
|
|
48
|
+
|
|
49
|
+
case DocstringText(text=text):
|
|
50
|
+
return "\n".join(f"{prefix}{part}" for part in text.split("\n"))
|
|
51
|
+
|
|
52
|
+
case DocstringSectionItem(text=text):
|
|
53
|
+
return "\n".join(f"{prefix}{part}" for part in text.split("\n"))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _is_single_line_docstring(
|
|
57
|
+
lines: tuple[DocstringLine, ...],
|
|
58
|
+
) -> bool:
|
|
59
|
+
return (
|
|
60
|
+
len(lines) == 1
|
|
61
|
+
and isinstance(lines[0], DocstringText)
|
|
62
|
+
and "\n" not in lines[0].text
|
|
63
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from gyomu_docstring.update.docstring.rendered_symbol import (
|
|
2
|
+
RenderedSymbolDocstring,
|
|
3
|
+
)
|
|
4
|
+
from gyomu_docstring.update.docstring.updated_docstring import UpdatedDocstring
|
|
5
|
+
from gyomu_docstring.update.internal.render_line import render_docstring_lines
|
|
6
|
+
from gyomu_docstring.update.internal.render_string import render_docstring_string
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def render_docstring(updated: UpdatedDocstring) -> RenderedSymbolDocstring:
|
|
10
|
+
lines = render_docstring_lines(updated)
|
|
11
|
+
|
|
12
|
+
document = render_docstring_string(
|
|
13
|
+
lines,
|
|
14
|
+
updated.docstring.location.start_offset
|
|
15
|
+
== updated.docstring.location.end_offset,
|
|
16
|
+
updated.docstring.indent,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
is_added = (
|
|
20
|
+
updated.docstring.location.start_offset == updated.docstring.location.end_offset
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
location = updated.docstring.location.model_copy()
|
|
24
|
+
|
|
25
|
+
location.start_offset -= updated.docstring.indent
|
|
26
|
+
|
|
27
|
+
if is_added:
|
|
28
|
+
location.end_offset -= updated.docstring.indent
|
|
29
|
+
|
|
30
|
+
return RenderedSymbolDocstring(
|
|
31
|
+
identity=updated.identity,
|
|
32
|
+
docstring=document,
|
|
33
|
+
location=location,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def render_docstrings(
|
|
38
|
+
updated_list: tuple[UpdatedDocstring, ...],
|
|
39
|
+
) -> tuple[RenderedSymbolDocstring, ...]:
|
|
40
|
+
return tuple(render_docstring(updated) for updated in updated_list)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: gyomu-docstring
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Gyomu docstring
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Requires-Dist: gyomu-infra
|
|
7
|
+
Requires-Dist: gyomu-python-analysis
|
|
8
|
+
Requires-Dist: gyomu-schema
|
|
9
|
+
Requires-Dist: returns>=0.29.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# gyomu-docstring
|
|
13
|
+
|
|
14
|
+
docstring components for Gyomu Python.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
gyomu_docstring/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
gyomu_docstring/update/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
gyomu_docstring/update/apply_file_update.py,sha256=v4sXzY1n5_06oeRqsYCSl3QuAsS6VZU2fbcNkAbm29Q,481
|
|
4
|
+
gyomu_docstring/update/apply_merge.py,sha256=NgNLED37kAYoLzFnEchKccDJ8aJNtAZFdnlekEsgzGw,13409
|
|
5
|
+
gyomu_docstring/update/build_file_update.py,sha256=zQLvosc7Pe8zJvGJ-rwOWgIP2P1SSbbWNOg80ZT8-EM,9047
|
|
6
|
+
gyomu_docstring/update/render_docstring.py,sha256=gYYZear0NlcsD-U6uNkAm-2pWS5Vkh0ZoeCwyB3ztHw,1269
|
|
7
|
+
gyomu_docstring/update/docstring/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
gyomu_docstring/update/docstring/file_update_plan.py,sha256=-Jckt8B0IwESoLkKJxIeGxD4xjrgsXv7pY2NgDiC0KU,364
|
|
9
|
+
gyomu_docstring/update/docstring/line.py,sha256=77areFAFYRww4l2yu-XZg5XPVHQgvMZ2bG3OYH05g9g,428
|
|
10
|
+
gyomu_docstring/update/docstring/merge_plan.py,sha256=ukgZf5b9RsbWRxTccPsOpcfDVrlPzzkLz0WKujxdI9I,2580
|
|
11
|
+
gyomu_docstring/update/docstring/rendered_symbol.py,sha256=B9ziDv3uvBVEKjkrvZ2LdJvrR60ggSlWxX7y_uWm4F8,298
|
|
12
|
+
gyomu_docstring/update/docstring/updated_docstring.py,sha256=2smj8x6uKAktUuQKhjirC1yhbi8g9FRPl8sprerNEYk,273
|
|
13
|
+
gyomu_docstring/update/internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
gyomu_docstring/update/internal/render_line.py,sha256=YeGyzWYZhEOa_-gWU8vbR_qr_Y-U5ywt8shmSCroQTM,5280
|
|
15
|
+
gyomu_docstring/update/internal/render_string.py,sha256=qbc9VF852YWhqbj85cY4qe-yiBXDYWoAl4mJXztqLO4,1480
|
|
16
|
+
gyomu_docstring-0.2.0.dist-info/METADATA,sha256=txj_wg4zyY6WQTTFkvuMZwSKPWXIsZSiQo0SMzYs384,330
|
|
17
|
+
gyomu_docstring-0.2.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
18
|
+
gyomu_docstring-0.2.0.dist-info/RECORD,,
|