neues skript sql_dump_diff.py zum Vergleich von 2 sql-dumps.

- leere, frisch initialisierte Datenbanken der Versionen verglichen
- datasetprogramme erkennen jetzt auch suche nach standardtabellen wie uom
This commit is contained in:
2026-08-22 17:45:46 +02:00
parent c0dd24b6c5
commit a55774eff2
19 changed files with 78583 additions and 269 deletions
+132 -31
View File
@@ -13,6 +13,18 @@ z. B. stock_split eine Klasse mit __name__ = 'stock.move' hinzu, landen
deren Felder unter demselben Modell-Eintrag, mit Vermerk, aus welchem
Modul sie stammen.
Zusätzlich wird ganz normale Python-Vererbung innerhalb eines Moduls
verfolgt: erbt eine Klasse mit eigenem __name__ von einer anderen lokalen
Klasse (z. B. `class BOMOutput(BOMInput): __name__ = 'production.bom.output'`
in production/bom.py, die selbst keine einzige fields.X(...)-Zeile
deklariert, sondern alle Felder von BOMInput übernimmt), werden die
geerbten Felder mit aufgenommen. Ohne das sähe ein rein über Vererbung
wiederverwendetes Modell wie ein Modell OHNE Felder aus - und Änderungen
daran (z. B. eine Feldumbenennung im Elternteil) blieben unsichtbar.
Löst nur Basisklassen auf, die im selben Modul per einfachem Namen (kein
Attribut-Zugriff auf ein anderes Modul) gefunden werden - externe Basen
wie ModelSQL/ModelView/Workflow tragen ohnehin keine fields.X(...) bei.
Zweck hier: BEVOR versucht wird, dieselbe dataset.yaml gegen 5.0 und 8.0
laufen zu lassen, erstmal Schwarz auf Weiß sehen, welche Modelle/Felder in
der 5.0-Codebasis (bei den tatsächlich aktiven Modulen) überhaupt
@@ -132,44 +144,117 @@ def is_persisted_class(node):
return False
def scan_file(path, module_name, module_dir, models, persisted):
def own_fields_of(node, module_name, module_dir, path):
"""Nur die in DIESEM Klassenkörper direkt deklarierten fields.X(...)."""
fields = {}
for stmt in node.body:
if not (
isinstance(stmt, ast.Assign)
and len(stmt.targets) == 1
and isinstance(stmt.targets[0], ast.Name)
):
continue
info = fields_call_info(stmt.value)
if info is None:
continue
field_type, relation, required, is_function, has_setter = info
fields[stmt.targets[0].id] = {
"type": field_type,
"relation": relation,
"required": required,
"settable": (not is_function) or has_setter,
"module": module_name,
"file": f"{module_name}/{path.relative_to(module_dir)}",
}
return fields
def base_class_names(node):
"""Best-effort Namen der Basisklassen als String, nur einfache
`class X(Y):`-Fälle (kein Attribut-Zugriff wie `other.Y`, das wäre
ohnehin eine externe/andere Modul-Klasse, die wir hier nicht kennen)."""
return [base.id for base in node.bases if isinstance(base, ast.Name)]
def collect_classes(path, module_name, module_dir, registry):
"""Erste Durchsicht: registriert jede Klasse (lokaler Python-Name,
Basisklassen, eigene Felder, ggf. Tryton-__name__, ggf. direkt
persistent) - unabhängig davon, ob/wie sie später gebraucht wird.
Die eigentliche Feld-/Persisted-Auflösung inkl. Vererbung passiert
danach in resolve_class_fields()/resolve_class_persisted().
Registry-Key ist (module_name, Dateipfad), NICHT nur module_name:
derselbe lokale Klassenname (z. B. "Cron") kommt innerhalb eines
Moduls in mehreren Dateien für kompott unterschiedliche Klassen vor
(Tryton-Konvention: eine PoolMeta-Erweiterung heißt oft genauso wie
das Original) - ein modulweites Registry würde sich da gegenseitig
überschreiben. Basisklassen-Auflösung bleibt dadurch bewusst auf
"gleiche Datei" beschränkt, was dem tatsächlichen Python-Scoping in
der weit überwiegenden Mehrheit der Fälle entspricht."""
try:
tree = ast.parse(path.read_text(encoding="utf-8"))
except (SyntaxError, UnicodeDecodeError) as e:
print(f" WARNUNG: {path} übersprungen ({e})", file=sys.stderr)
return
file_key = (module_name, str(path))
for node in ast.walk(tree):
if not isinstance(node, ast.ClassDef):
continue
model_name = find_name_assignment(node.body)
if not model_name:
continue
registry.setdefault(file_key, {})[node.name] = {
"bases": base_class_names(node),
"own_fields": own_fields_of(node, module_name, module_dir, path),
"model_name": find_name_assignment(node.body),
"persisted_here": is_persisted_class(node),
}
if is_persisted_class(node):
persisted.add(model_name)
model = models.setdefault(model_name, {})
for stmt in node.body:
if not (
isinstance(stmt, ast.Assign)
and len(stmt.targets) == 1
and isinstance(stmt.targets[0], ast.Name)
):
continue
field_name = stmt.targets[0].id
info = fields_call_info(stmt.value)
if info is None:
continue
field_type, relation, required, is_function, has_setter = info
model[field_name] = {
"type": field_type,
"relation": relation,
"required": required,
"settable": (not is_function) or has_setter,
"module": module_name,
"file": f"{module_name}/{path.relative_to(module_dir)}",
}
def resolve_class_fields(scope_key, class_name, registry, cache, visiting):
"""Felder einer lokalen Klasse inkl. per Basisklasse(n) in derselben
Datei (scope_key = (module_name, Dateipfad)) geerbter Felder (eigene
Felder überschreiben geerbte bei Namensgleichheit, wie bei echter
Python-Attributauflösung)."""
key = (scope_key, class_name)
if key in cache:
return cache[key]
if key in visiting:
return {} # zyklische Vererbung - sollte nicht vorkommen, Notbremse
class_info = registry.get(scope_key, {}).get(class_name)
if class_info is None:
return {} # externe/unbekannte Basisklasse (ModelSQL, Workflow, ...)
visiting.add(key)
fields = {}
for base_name in class_info["bases"]:
fields.update(resolve_class_fields(scope_key, base_name, registry, cache, visiting))
fields.update(class_info["own_fields"])
visiting.discard(key)
cache[key] = fields
return fields
def resolve_class_persisted(scope_key, class_name, registry, cache, visiting):
"""True, wenn diese Klasse selbst oder eine ihrer lokalen Basisklassen
ModelSQL/Workflow/DeactivableMixin als Basis hat."""
key = (scope_key, class_name)
if key in cache:
return cache[key]
if key in visiting:
return False
class_info = registry.get(scope_key, {}).get(class_name)
if class_info is None:
return False
visiting.add(key)
result = class_info["persisted_here"] or any(
resolve_class_persisted(scope_key, base_name, registry, cache, visiting)
for base_name in class_info["bases"]
)
visiting.discard(key)
cache[key] = result
return result
def module_directory(repo, module_name):
@@ -181,8 +266,8 @@ def module_directory(repo, module_name):
def scan_modules(repo, modules):
models = {}
persisted = set()
# Phase 1: alle Klassen roh einsammeln (Basen, eigene Felder, __name__).
registry = {}
for module_name in modules:
module_dir = module_directory(repo, module_name)
if not module_dir.is_dir():
@@ -191,10 +276,26 @@ def scan_modules(repo, modules):
for path in sorted(module_dir.rglob("*.py")):
if "tests" in path.relative_to(module_dir).parts:
continue
scan_file(path, module_name, module_dir, models, persisted)
collect_classes(path, module_name, module_dir, registry)
# Phase 2: pro Klasse mit __name__ die Felder (inkl. lokaler Python-
# Vererbung) auflösen und unter dem Tryton-Modellnamen zusammenführen -
# mehrere Klassen mit demselben __name__ (PoolMeta-Erweiterungen aus
# anderen Modulen) tragen weiterhin gemeinsam zum selben Eintrag bei.
fields_cache, persisted_cache = {}, {}
models = {}
persisted_models = set()
for scope_key, classes in registry.items():
for class_name, info in classes.items():
if not info["model_name"]:
continue
resolved = resolve_class_fields(scope_key, class_name, registry, fields_cache, set())
models.setdefault(info["model_name"], {}).update(resolved)
if resolve_class_persisted(scope_key, class_name, registry, persisted_cache, set()):
persisted_models.add(info["model_name"])
return {
model_name: {"persisted": model_name in persisted, "fields": fields}
model_name: {"persisted": model_name in persisted_models, "fields": fields}
for model_name, fields in models.items()
}