I just vibe coded the following script:
#!/usr/bin/env python3
"""
convert_tryton_register.py
Convert Tryton 7.6-style __init__.py Pool.register() declarations into a
Tryton 7.8+ tryton.cfg [register] section.
Usage:
# Print the [register] section to stdout
python convert_tryton_register.py --init path/to/__init__.py
# Replace (or insert) [register] in an existing tryton.cfg and write to output
python convert_tryton_register.py --init path/to/__init__.py \
--cfg-in path/to/tryton.cfg --cfg-out path/to/tryton.cfg
Notes:
- This script only generates the [register] section (model/wizard/report),
because that is what can be derived from __init__.py reliably.
- It parses source code using AST; it does not import or execute your module.
"""
from __future__ import annotations
import argparse
import ast
import re
from collections import OrderedDict
from pathlib import Path
from typing import Dict, List, Tuple
REGISTER_TYPES = ("model", "wizard", "report")
def dotted_name(node: ast.AST) -> str:
"""
Return a dotted name like 'account.Account' from an AST node.
Falls back to ast.unparse for unexpected shapes.
"""
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
left = dotted_name(node.value)
return f"{left}.{node.attr}" if left else node.attr
if isinstance(node, ast.Call):
# Unlikely in Pool.register args; fall back
return ast.unparse(node)
if isinstance(node, ast.Constant) and isinstance(node.value, str):
return node.value
return ast.unparse(node)
def is_pool_register_call(call: ast.Call) -> bool:
"""
True for Pool.register(...)
"""
f = call.func
return (
isinstance(f, ast.Attribute)
and f.attr == "register"
and isinstance(f.value, ast.Name)
and f.value.id == "Pool"
)
def extract_register_calls(init_source: str) -> List[ast.Call]:
"""
Find Pool.register(...) calls inside def register(): ...
"""
tree = ast.parse(init_source)
register_func: ast.FunctionDef | None = None
for node in tree.body:
if isinstance(node, ast.FunctionDef) and node.name == "register":
register_func = node
break
if register_func is None:
raise ValueError("Could not find a function named register() in __init__.py")
calls: List[ast.Call] = []
for stmt in register_func.body:
# Expect statements like: Pool.register(...)
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
call = stmt.value
if is_pool_register_call(call):
calls.append(call)
if not calls:
raise ValueError("No Pool.register(...) calls found inside register()")
return calls
def extract_type_keyword(call: ast.Call) -> str:
"""
Read keyword type_='model'/'wizard'/'report'
"""
for kw in call.keywords:
if kw.arg == "type_":
if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
return kw.value.value
return ast.unparse(kw.value).strip("'\"")
raise ValueError("Pool.register(...) call is missing keyword argument type_=")
def extract_registered_objects(call: ast.Call) -> List[str]:
"""
Positional args are the model/wizard/report classes, e.g. account.Account, move.Move, ...
Keywords (module/type_) are ignored for list generation.
"""
items: List[str] = []
for arg in call.args:
if isinstance(arg, ast.Starred):
raise ValueError("Starred arguments (*something) are not supported in Pool.register args.")
items.append(dotted_name(arg))
return items
def build_register_map(calls: List[ast.Call]) -> Dict[str, List[str]]:
"""
Return {"model":[...], "wizard":[...], "report":[...]} preserving order and removing duplicates.
"""
buckets: Dict[str, "OrderedDict[str, None]"] = {
t: OrderedDict() for t in REGISTER_TYPES
}
for call in calls:
t = extract_type_keyword(call)
if t not in buckets:
# Keep unknown types too, but place them after known ones
buckets.setdefault(t, OrderedDict())
for item in extract_registered_objects(call):
buckets[t][item] = None
return {t: list(od.keys()) for t, od in buckets.items() if od}
def render_register_section(reg: Dict[str, List[str]]) -> str:
"""
Render in tryton.cfg style:
[register]
model:
a.b
c.d
wizard:
...
"""
lines: List[str] = ["[register]"]
for t in REGISTER_TYPES:
if t in reg and reg[t]:
lines.append(f"{t}:")
lines.extend([f" {name}" for name in reg[t]])
# Include any non-standard types (if ever present), in stable order at the end
for t in reg.keys():
if t not in REGISTER_TYPES:
lines.append(f"{t}:")
lines.extend([f" {name}" for name in reg[t]])
return "\n".join(lines) + "\n"
REGISTER_SECTION_RE = re.compile(
r"(?ms)^\[register\]\s*\n.*?(?=^\[[^\]]+\]\s*$|\Z)"
)
def replace_or_insert_register(cfg_text: str, new_register_section: str) -> str:
"""
Replace existing [register] section if present; otherwise append at end with a blank line.
"""
if REGISTER_SECTION_RE.search(cfg_text):
return REGISTER_SECTION_RE.sub(new_register_section.rstrip() + "\n", cfg_text)
# If there's no [register], append it at the end with spacing
suffix = "" if cfg_text.endswith("\n") else "\n"
return cfg_text + suffix + "\n" + new_register_section
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--init", required=True, help="Path to __init__.py (Tryton 7.6 style)")
ap.add_argument("--cfg-in", help="Optional: existing tryton.cfg to update")
ap.add_argument("--cfg-out", help="Optional: output path for updated tryton.cfg")
args = ap.parse_args()
init_path = Path(args.init)
init_source = init_path.read_text(encoding="utf-8")
calls = extract_register_calls(init_source)
regmap = build_register_map(calls)
register_section = render_register_section(regmap)
if args.cfg_in:
cfg_in = Path(args.cfg_in)
cfg_text = cfg_in.read_text(encoding="utf-8")
updated = replace_or_insert_register(cfg_text, register_section)
if args.cfg_out:
Path(args.cfg_out).write_text(updated, encoding="utf-8")
else:
# If cfg-in is provided but cfg-out is not, print updated cfg to stdout
print(updated, end="")
else:
# No cfg input: just print the register section
print(register_section, end="")
if __name__ == "__main__":
main()
Feel free to run it, improve it and share it as you wish.
For now I just tested with account module, I will test on our custom modules when we start migrating them to 7.8 series.
If you want to share it under some tryton community repository I will take care of maintaining it if I found some issues on any modules.