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:
+132
-31
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user