This commit is contained in:
2026-07-16 23:04:28 +08:00
parent 45f0095036
commit f224eee633
+66
View File
@@ -337,6 +337,69 @@ def _append_missing_methods(
return runtime_block.rstrip() + "\n\n" + "".join(additions).rstrip() + "\n" return runtime_block.rstrip() + "\n\n" + "".join(additions).rstrip() + "\n"
def _sync_generated_method_docstrings(
runtime_block: str, generated_block: str, method_names: set[str]
) -> str:
"""Use generated method docstrings while preserving runtime method bodies."""
runtime_class = ast.parse(runtime_block).body[0]
generated_class = ast.parse(generated_block).body[0]
if not isinstance(runtime_class, ast.ClassDef) or not isinstance(generated_class, ast.ClassDef):
return runtime_block
runtime_methods = {
statement.name: statement
for statement in runtime_class.body
if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef))
}
generated_methods = {
statement.name: statement
for statement in generated_class.body
if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef))
}
runtime_lines = runtime_block.splitlines(keepends=True)
generated_lines = generated_block.splitlines(keepends=True)
replacements: list[tuple[int, int, list[str]]] = []
def docstring_statement(method: ast.FunctionDef | ast.AsyncFunctionDef) -> ast.Expr | None:
if not method.body:
return None
statement = method.body[0]
if (
isinstance(statement, ast.Expr)
and isinstance(statement.value, ast.Constant)
and isinstance(statement.value.value, str)
):
return statement
return None
for name in method_names:
runtime_method = runtime_methods.get(name)
generated_method = generated_methods.get(name)
if runtime_method is None or generated_method is None:
continue
generated_doc = docstring_statement(generated_method)
if generated_doc is None:
continue
generated_source = generated_lines[
generated_doc.lineno - 1:generated_doc.end_lineno
]
runtime_doc = docstring_statement(runtime_method)
if runtime_doc is not None:
replacements.append(
(runtime_doc.lineno - 1, runtime_doc.end_lineno, generated_source)
)
else:
# A method without a docstring starts directly with executable
# code. Insert the generated VP description before that code.
replacements.append(
(runtime_method.body[0].lineno - 1, runtime_method.body[0].lineno - 1, generated_source)
)
for start, end, replacement in sorted(replacements, reverse=True):
runtime_lines[start:end] = replacement
return "".join(runtime_lines)
def _ensure_get_data_import(source: str) -> str: def _ensure_get_data_import(source: str) -> str:
if not re.search(r"(?m)^\s*data\d+\s*=\s*get_data\(", source): if not re.search(r"(?m)^\s*data\d+\s*=\s*get_data\(", source):
return source return source
@@ -433,6 +496,9 @@ def merge(generated_path: Path, character_path: Path) -> tuple[str, dict[str, in
block = _set_generated_field(block, "position", generated_skill.position) block = _set_generated_field(block, "position", generated_skill.position)
block = _set_generated_field(block, "cellPos", generated_skill.cell_pos) block = _set_generated_field(block, "cellPos", generated_skill.cell_pos)
block = _append_missing_methods(block, generated_block, {"vp_1", "vp_2"}) block = _append_missing_methods(block, generated_block, {"vp_1", "vp_2"})
block = _sync_generated_method_docstrings(
block, generated_block, {"vp_1", "vp_2"}
)
chunks.append(block.rstrip() + "\n") chunks.append(block.rstrip() + "\n")
matched += 1 matched += 1
cursor = generated_skill.end cursor = generated_skill.end