From 7bf43718e0134fb532231863d072f21f70d7ad3c Mon Sep 17 00:00:00 2001 From: Ulrich Date: Fri, 7 Aug 2026 21:06:43 +0200 Subject: [PATCH] first working version of extractor --- data/8.0.8/party.json | 200 +++++++++++++++++++++++++++++++++ extract.py | 253 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 453 insertions(+) create mode 100644 data/8.0.8/party.json create mode 100644 extract.py diff --git a/data/8.0.8/party.json b/data/8.0.8/party.json new file mode 100644 index 0000000..c25e487 --- /dev/null +++ b/data/8.0.8/party.json @@ -0,0 +1,200 @@ +{ + "version": "8.0.8", + "module": "party", + "files": [ + { + "file": "/home/uha4/tmp/tryton/modules/party/address.py", + "class": "Address", + "method": "__register__", + "line": 161, + "operations": [ + { + "object": "table", + "operation": "column_rename", + "line": 166, + "args": [ + "'street'", + "'street_unstructured'" + ], + "kwargs": {}, + "code": "table.column_rename('street', 'street_unstructured')", + "phase": "before_super" + }, + { + "object": "table", + "operation": "column_rename", + "line": 167, + "args": [ + "'name'", + "'building_name'" + ], + "kwargs": {}, + "code": "table.column_rename('name', 'building_name')", + "phase": "before_super" + } + ] + }, + { + "file": "/home/uha4/tmp/tryton/modules/party/configuration.py", + "class": "Configuration", + "method": "__register__", + "line": 33, + "operations": [ + { + "object": "cursor", + "operation": "execute", + "line": 44, + "args": [ + "*table.update([table.identifier_types], [cls.identifier_types.sql_format(identifier_types)])" + ], + "kwargs": {}, + "code": "cursor.execute(*table.update([table.identifier_types], [cls.identifier_types.sql_format(identifier_types)]))", + "phase": "after_super" + }, + { + "object": "table", + "operation": "update", + "line": 44, + "args": [ + "[table.identifier_types]", + "[cls.identifier_types.sql_format(identifier_types)]" + ], + "kwargs": {}, + "code": "table.update([table.identifier_types], [cls.identifier_types.sql_format(identifier_types)])", + "phase": "after_super" + } + ] + }, + { + "file": "/home/uha4/tmp/tryton/modules/party/party.py", + "class": "Identifier", + "method": "__register__", + "line": 859, + "operations": [ + { + "object": "cursor", + "operation": "execute", + "line": 873, + "args": [ + "*table.update([table.type], [new], where=table.type == old)" + ], + "kwargs": {}, + "code": "cursor.execute(*table.update([table.type], [new], where=table.type == old))", + "phase": "after_super" + }, + { + "object": "table", + "operation": "update", + "line": 873, + "args": [ + "[table.type]", + "[new]" + ], + "kwargs": { + "where": "table.type == old" + }, + "code": "table.update([table.type], [new], where=table.type == old)", + "phase": "after_super" + }, + { + "object": "cursor", + "operation": "execute", + "line": 878, + "args": [ + "*table.update([table.type], ['uy_rut'], where=table.type == 'uy_ruc')" + ], + "kwargs": {}, + "code": "cursor.execute(*table.update([table.type], ['uy_rut'], where=table.type == 'uy_ruc'))", + "phase": "after_super" + }, + { + "object": "table", + "operation": "update", + "line": 878, + "args": [ + "[table.type]", + "['uy_rut']" + ], + "kwargs": { + "where": "table.type == 'uy_ruc'" + }, + "code": "table.update([table.type], ['uy_rut'], where=table.type == 'uy_ruc')", + "phase": "after_super" + }, + { + "object": "cursor", + "operation": "execute", + "line": 882, + "args": [ + "*table.update([table.type], ['co_nit'], where=table.type == 'co_rut')" + ], + "kwargs": {}, + "code": "cursor.execute(*table.update([table.type], ['co_nit'], where=table.type == 'co_rut'))", + "phase": "after_super" + }, + { + "object": "table", + "operation": "update", + "line": 882, + "args": [ + "[table.type]", + "['co_nit']" + ], + "kwargs": { + "where": "table.type == 'co_rut'" + }, + "code": "table.update([table.type], ['co_nit'], where=table.type == 'co_rut')", + "phase": "after_super" + }, + { + "object": "cursor", + "operation": "execute", + "line": 887, + "args": [ + "*table.update([table.code_compact], [table.code])" + ], + "kwargs": {}, + "code": "cursor.execute(*table.update([table.code_compact], [table.code]))", + "phase": "after_super" + }, + { + "object": "table", + "operation": "update", + "line": 887, + "args": [ + "[table.code_compact]", + "[table.code]" + ], + "kwargs": {}, + "code": "table.update([table.code_compact], [table.code])", + "phase": "after_super" + }, + { + "object": "cursor", + "operation": "execute", + "line": 890, + "args": [ + "*table.update([table.type], ['si_maticna'], where=table.type == 'si_businessid')" + ], + "kwargs": {}, + "code": "cursor.execute(*table.update([table.type], ['si_maticna'], where=table.type == 'si_businessid'))", + "phase": "after_super" + }, + { + "object": "table", + "operation": "update", + "line": 890, + "args": [ + "[table.type]", + "['si_maticna']" + ], + "kwargs": { + "where": "table.type == 'si_businessid'" + }, + "code": "table.update([table.type], ['si_maticna'], where=table.type == 'si_businessid')", + "phase": "after_super" + } + ] + } + ] +} \ No newline at end of file diff --git a/extract.py b/extract.py new file mode 100644 index 0000000..d07b08d --- /dev/null +++ b/extract.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 + +import ast +import argparse +import json +import re +from pathlib import Path + +DEFAULT_REPO = "/home/uha4/tmp/tryton" + +DEFAULT_MODULES = [ + "party", +] + +DEFAULT_OUTPUT = "data" + +class RegisterExtractor(ast.NodeVisitor): + + def __init__(self, filename): + self.filename = filename + self.results = [] + + def visit_ClassDef(self, node): + for item in node.body: + if isinstance(item, ast.FunctionDef) and item.name == "__register__": + self.extract_register(node.name, item) + + self.generic_visit(node) + + def extract_register(self, cls, func): + + phase = "before_super" + + operations = [] + + for stmt in func.body: + + # Wurde super().__register__() aufgerufen? + if self.is_super_register(stmt): + phase = "after_super" + continue + + for call in ast.walk(stmt): + if isinstance(call, ast.Call): + + op = self.extract_call(call) + + if op: + op["phase"] = phase + operations.append(op) + + self.results.append({ + "file": self.filename, + "class": cls, + "method": "__register__", + "line": func.lineno, + "operations": operations, + }) + + def is_super_register(self, stmt): + + if not isinstance(stmt, ast.Expr): + return False + + call = stmt.value + + if not isinstance(call, ast.Call): + return False + + func = call.func + + if not isinstance(func, ast.Attribute): + return False + + if func.attr != "__register__": + return False + + if not isinstance(func.value, ast.Call): + return False + + if not isinstance(func.value.func, ast.Name): + return False + + return func.value.func.id == "super" + + def extract_call(self, call): + + if not isinstance(call.func, ast.Attribute): + return None + + interesting = { + "column_rename", + "drop_column", + "add_column", + "drop_constraint", + "add_constraint", + "not_null_action", + "index_action", + "alter_size", + "execute", + "executemany", + "update", + "insert", + "delete", + "select", + } + + operation = call.func.attr + + if operation not in interesting: + return None + + # Wer wird aufgerufen? + if isinstance(call.func.value, ast.Name): + obj = call.func.value.id + else: + obj = ast.unparse(call.func.value) + + args = [] + + for arg in call.args: + try: + args.append(ast.unparse(arg)) + except Exception: + args.append("") + + kwargs = {} + + for kw in call.keywords: + try: + kwargs[kw.arg] = ast.unparse(kw.value) + except Exception: + kwargs[kw.arg] = "" + + return { + "object": obj, + "operation": operation, + "line": call.lineno, + "args": args, + "kwargs": kwargs, + "code": ast.unparse(call), + } + +def analyse_file(path): + + try: + tree = ast.parse(path.read_text(encoding="utf-8")) + except Exception as e: + print(f"Cannot parse {path}: {e}") + return [] + + visitor = RegisterExtractor(str(path)) + visitor.visit(tree) + + results = [r for r in visitor.results if r["operations"]] + + return results + +def detect_version(repo: Path) -> str: + changelog = repo / "trytond" / "CHANGELOG" + + if not changelog.exists(): + return "unknown" + + with changelog.open("r", encoding="utf-8") as f: + for line in f: + if line.startswith("Version "): + return line.split()[1] + + return "unknown" + +def find_module(repo: Path, module: str) -> Path | None: + + candidates = [ + repo / "modules" / module, + repo / "trytond" / "modules" / module, + ] + + for c in candidates: + if c.exists(): + return c + + return None + +def main(): + + parser = argparse.ArgumentParser() + + parser.add_argument( + "--repo", + help="Path to Tryton repository", + ) + + parser.add_argument( + "--modules", + nargs="+", + help="Module names", + ) + + parser.add_argument( + "--output", + help="Output directory", + ) + + args = parser.parse_args() + + repo = Path(args.repo or DEFAULT_REPO) + + modules = args.modules or DEFAULT_MODULES + + output = Path(args.output or DEFAULT_OUTPUT) + + version = detect_version(repo) + + print(f"Repository : {repo}") + print(f"Version : {version}") + print(f"Modules : {', '.join(modules)}") + print() + + for module in modules: + + module_path = repo / "modules" / module + + if not module_path.exists(): + print(f"Module not found: {module}") + continue + + results = [] + + for py in sorted(module_path.rglob("*.py")): + results.extend(analyse_file(py)) + + outdir = output / version + outdir.mkdir(parents=True, exist_ok=True) + + outfile = outdir / f"{module}.json" + + with outfile.open("w", encoding="utf-8") as f: + json.dump( + { + "version": version, + "module": module, + "files": results, + }, + f, + indent=2, + ) + + print(f"{module:25} -> {outfile}") + + +if __name__ == "__main__": + main() \ No newline at end of file