From f224eee6331a0d414563765430d21d7166aa2aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E7=93=9C=C2=B0?= Date: Thu, 16 Jul 2026 23:04:28 +0800 Subject: [PATCH] init --- scripts/migrate_generated_characters.py | 66 +++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/scripts/migrate_generated_characters.py b/scripts/migrate_generated_characters.py index c8215ab7..4c08f66f 100644 --- a/scripts/migrate_generated_characters.py +++ b/scripts/migrate_generated_characters.py @@ -337,6 +337,69 @@ def _append_missing_methods( 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: if not re.search(r"(?m)^\s*data\d+\s*=\s*get_data\(", 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, "cellPos", generated_skill.cell_pos) 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") matched += 1 cursor = generated_skill.end