v0.6.1
Fix gui_schemes
This commit is contained in:
+95
-55
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user