v0.6.1
Example document / build (push) Has been cancelled
Python application / build (push) Has been cancelled

Fix gui_schemes
This commit is contained in:
Igor20264
2026-10-20 21:53:34 +03:00
parent 8223e0ac14
commit 8fc1f9e096
6 changed files with 253 additions and 86 deletions
+95 -55
View File
@@ -114,67 +114,48 @@ def is_app_owned_scheme(scheme: DiagramScheme) -> bool:
return scheme.author.strip().lower() == APP_SCHEME_AUTHOR
def _schemes_payload(schemes: dict[str, DiagramScheme]) -> dict[str, Any]:
return {sid: scheme.to_dict() for sid, scheme in sorted(schemes.items())}
def migrate_user_schemes(base: Path | None = None) -> SchemesMigrationResult:
"""
Sync app-owned schemes in the user file with the bundled template.
Add bundled schemes that are missing from the user file.
- Bundled ids: always present; overwrite user entries with author=md2gost.
- User overrides of bundled ids (empty/other author): kept as-is.
- Orphaned author=md2gost ids no longer in the template: removed.
- Other custom schemes: kept.
Existing entries are kept as written: edits of built-in ids, custom
schemes, and schemes with author=md2gost are not overwritten or removed.
A file that cannot be parsed is left untouched.
"""
result = SchemesMigrationResult()
path = user_schemes_path(base)
if not path.is_file():
return result
bundled = _load_schemes_file(bundled_schemes_path())
user = _load_schemes_file(path)
out: dict[str, DiagramScheme] = {}
for sid, scheme in user.items():
if sid in bundled:
if is_app_owned_scheme(scheme):
continue # step below writes bundled
out[sid] = scheme
continue
if is_app_owned_scheme(scheme):
result.removed.append(sid)
_log.info("Миграция схем: удалена устаревшая встроенная «%s»", sid)
continue
out[sid] = scheme
for sid, scheme in bundled.items():
if sid in out:
continue # user-owned override
prev = user.get(sid)
if prev is None:
result.added.append(sid)
_log.info("Миграция схем: добавлена «%s»", sid)
elif prev.to_dict() != scheme.to_dict():
result.updated.append(sid)
_log.info("Миграция схем: обновлена «%s»", sid)
out[sid] = scheme
if _schemes_payload(out) == _schemes_payload(user):
result.added.clear()
result.updated.clear()
result.removed.clear()
raw, err = _read_json_object(path)
if err or raw is None:
_log.warning("Миграция схем пропущена, файл не изменён (%s): %s", path, err)
return result
save_schemes(out, path)
bundled = _load_schemes_file(bundled_schemes_path())
existing = {str(key).lower().strip() for key in raw}
for sid, scheme in bundled.items():
if sid in existing:
continue
raw[sid] = scheme.to_dict()
result.added.append(sid)
_log.info("Миграция схем: добавлена «%s»", sid)
if not result.added:
return result
path.write_text(
json.dumps(raw, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
result.written = True
return result
def ensure_user_schemes(base: Path | None = None) -> Path:
"""
Ensure md2gost.schemes.json exists next to the app, then sync app-owned
schemes from the bundled template (add / update / remove orphans).
Ensure md2gost.schemes.json exists next to the app, then add any
bundled schemes that are not already in the user file.
"""
dest = user_schemes_path(base)
if not dest.is_file():
@@ -190,16 +171,21 @@ def ensure_user_schemes(base: Path | None = None) -> Path:
return dest
def _load_schemes_file(path: Path) -> dict[str, DiagramScheme]:
def _read_json_object(path: Path) -> tuple[dict[str, Any] | None, str | None]:
"""Return (object, None). Missing file is ({}, None). Unreadable is (None, error)."""
if not path.is_file():
return {}
return {}, None
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
_log.warning("Не удалось прочитать схемы %s: %s", path, exc)
return {}
# utf-8-sig: Notepad on Windows often writes a BOM, which json.loads rejects.
raw = json.loads(path.read_text(encoding="utf-8-sig"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
return None, str(exc)
if not isinstance(raw, dict):
return {}
return None, "корень JSON должен быть объектом"
return raw, None
def _schemes_from_raw(raw: dict[str, Any]) -> dict[str, DiagramScheme]:
out: dict[str, DiagramScheme] = {}
for key, val in raw.items():
sid = str(key).lower().strip()
@@ -212,16 +198,70 @@ def _load_schemes_file(path: Path) -> dict[str, DiagramScheme]:
return out
def _load_schemes_file(path: Path) -> dict[str, DiagramScheme]:
raw, err = _read_json_object(path)
if err or raw is None:
if path.is_file():
_log.warning("Не удалось прочитать схемы %s: %s", path, err)
return {}
return _schemes_from_raw(raw)
def load_schemes_from_path(path: Path) -> dict[str, DiagramScheme]:
return _load_schemes_file(path)
def save_schemes(schemes: dict[str, DiagramScheme], path: Path | None = None) -> Path:
def load_schemes_report(path: Path) -> tuple[dict[str, DiagramScheme], str | None]:
"""Load schemes. Error string is set when the file exists but cannot be parsed."""
raw, err = _read_json_object(path)
if err or raw is None:
if path.is_file():
_log.warning("Не удалось прочитать схемы %s: %s", path, err)
return {}, err
return _schemes_from_raw(raw), None
def save_schemes(
schemes: dict[str, DiagramScheme],
path: Path | None = None,
*,
removed_ids: set[str] | None = None,
) -> Path:
"""
Write schemes, keeping JSON entries the loader does not understand
(unknown ids, extra fields). ``removed_ids`` are normalized ids the user
deleted or renamed away.
"""
dest = path or user_schemes_path()
payload = _schemes_payload(schemes)
dest.parent.mkdir(parents=True, exist_ok=True)
raw, err = _read_json_object(dest)
if err or raw is None:
raw = {}
removed = {sid.lower() for sid in (removed_ids or set())}
norm_to_key: dict[str, str] = {}
for key in list(raw):
sid = str(key).lower().strip()
if sid in removed:
del raw[key]
continue
norm_to_key.setdefault(sid, str(key))
for sid in sorted(schemes):
scheme = schemes[sid]
data = scheme.to_dict()
key = norm_to_key.get(sid)
if key is None:
raw[sid] = data
continue
base = raw.get(key)
merged = dict(base) if isinstance(base, dict) else {}
merged.update(data)
if not scheme.theme:
merged.pop("theme", None)
raw[key] = merged
dest.write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
json.dumps(raw, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
return dest
@@ -234,7 +274,7 @@ def claim_user_scheme_if_diverged(
) -> DiagramScheme:
"""
If scheme id is in the app template and content differs while still
author=md2gost, clear author so migration will not overwrite user edits.
author=md2gost, clear author so the copy is marked as the user's.
"""
stock = (bundled if bundled is not None else _load_schemes_file(bundled_schemes_path())).get(
scheme.id