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.
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any