first working version of extractor

This commit is contained in:
2026-08-07 21:06:43 +02:00
parent d3ac24f2a9
commit 7bf43718e0
2 changed files with 453 additions and 0 deletions
+200
View File
@@ -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"
}
]
}
]
}
+253
View File
@@ -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("<unknown>")
kwargs = {}
for kw in call.keywords:
try:
kwargs[kw.arg] = ast.unparse(kw.value)
except Exception:
kwargs[kw.arg] = "<unknown>"
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()