v0.6.1
Fix gui_schemes
This commit is contained in:
+1
-1
@@ -655,7 +655,7 @@ Rel_Triggering(investigate, close)
|
|||||||
- Полная Appendix B в sample отсутствует — матрица пар здесь практическая, не нормативная таблица B.5.
|
- Полная Appendix B в sample отсутствует — матрица пар здесь практическая, не нормативная таблица B.5.
|
||||||
- PDF спецификации в git не хранятся.
|
- PDF спецификации в git не хранятся.
|
||||||
- Старый PlantUML/Kroki без stdlib 3.2: fallback — `` ```uml `` + keyword `archimate`.
|
- Старый PlantUML/Kroki без stdlib 3.2: fallback — `` ```uml `` + keyword `archimate`.
|
||||||
- Пользовательский `md2gost.schemes.json`: встроенные схемы с `author: md2gost` синхронизируются с бандлом; свои правки (другой author) не затираются.
|
- Пользовательский `md2gost.schemes.json` при запуске только дополняется отсутствующими встроенными схемами. Уже записанные правки не затираются.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -38,7 +38,7 @@ Rel(user, app, "логин")
|
|||||||
|
|
||||||
## Встроенные схемы
|
## Встроенные схемы
|
||||||
|
|
||||||
Файл шаблона: [`md2gost/diagrams/schemes.json`](../md2gost/diagrams/schemes.json). При первом запуске копируется в `md2gost.schemes.json` рядом с приложением. При каждом запуске встроенные схемы (`author: md2gost`) синхронизируются с шаблоном: новые id добавляются, устаревшие (например удалённый `bpmn`) убираются, содержимое обновляется. Схемы с пустым или другим `author` не трогаются. Если правите встроенную в GUI и сохраняете — `author` сбрасывается, чтобы миграция не затёрла правки.
|
Файл шаблона: [`md2gost/diagrams/schemes.json`](../md2gost/diagrams/schemes.json). При первом запуске копируется в `md2gost.schemes.json` рядом с приложением. При каждом запуске в файл добавляются только отсутствующие встроенные схемы. Уже записанные схемы не перезаписываются и не удаляются: правки и свои шаблоны сохраняются. Файл, который не удалось разобрать, не затирается. Если правите встроенную схему в GUI и сохраняете — `author` сбрасывается, схема помечается как ваша.
|
||||||
|
|
||||||
| id | Название | Оградка |
|
| id | Название | Оградка |
|
||||||
|----|----------|---------|
|
|----|----------|---------|
|
||||||
@@ -291,4 +291,4 @@ id схемы: латиница, цифры, `_`; начинается с бук
|
|||||||
2. Или править `md2gost.schemes.json` вручную («Открыть JSON»).
|
2. Или править `md2gost.schemes.json` вручную («Открыть JSON»).
|
||||||
3. CLI: `--schemes путь.json`.
|
3. CLI: `--schemes путь.json`.
|
||||||
|
|
||||||
Чтобы правка встроенной схемы не сбрасывалась при обновлении приложения, оставьте пустой `author` (GUI делает это сама при сохранении изменённой встроенной схемы).
|
Запуск программы не сбрасывает правки в `md2gost.schemes.json`. В GUI кнопка «Добавить» сразу записывает новую схему (`custom1`, `custom2`, …) в этот файл.
|
||||||
|
|||||||
+95
-55
@@ -114,67 +114,48 @@ def is_app_owned_scheme(scheme: DiagramScheme) -> bool:
|
|||||||
return scheme.author.strip().lower() == APP_SCHEME_AUTHOR
|
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:
|
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.
|
Existing entries are kept as written: edits of built-in ids, custom
|
||||||
- User overrides of bundled ids (empty/other author): kept as-is.
|
schemes, and schemes with author=md2gost are not overwritten or removed.
|
||||||
- Orphaned author=md2gost ids no longer in the template: removed.
|
A file that cannot be parsed is left untouched.
|
||||||
- Other custom schemes: kept.
|
|
||||||
"""
|
"""
|
||||||
result = SchemesMigrationResult()
|
result = SchemesMigrationResult()
|
||||||
path = user_schemes_path(base)
|
path = user_schemes_path(base)
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
return result
|
return result
|
||||||
|
|
||||||
bundled = _load_schemes_file(bundled_schemes_path())
|
raw, err = _read_json_object(path)
|
||||||
user = _load_schemes_file(path)
|
if err or raw is None:
|
||||||
out: dict[str, DiagramScheme] = {}
|
_log.warning("Миграция схем пропущена, файл не изменён (%s): %s", path, err)
|
||||||
|
|
||||||
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()
|
|
||||||
return result
|
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
|
result.written = True
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def ensure_user_schemes(base: Path | None = None) -> Path:
|
def ensure_user_schemes(base: Path | None = None) -> Path:
|
||||||
"""
|
"""
|
||||||
Ensure md2gost.schemes.json exists next to the app, then sync app-owned
|
Ensure md2gost.schemes.json exists next to the app, then add any
|
||||||
schemes from the bundled template (add / update / remove orphans).
|
bundled schemes that are not already in the user file.
|
||||||
"""
|
"""
|
||||||
dest = user_schemes_path(base)
|
dest = user_schemes_path(base)
|
||||||
if not dest.is_file():
|
if not dest.is_file():
|
||||||
@@ -190,16 +171,21 @@ def ensure_user_schemes(base: Path | None = None) -> Path:
|
|||||||
return dest
|
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():
|
if not path.is_file():
|
||||||
return {}
|
return {}, None
|
||||||
try:
|
try:
|
||||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
# utf-8-sig: Notepad on Windows often writes a BOM, which json.loads rejects.
|
||||||
except (OSError, json.JSONDecodeError) as exc:
|
raw = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||||
_log.warning("Не удалось прочитать схемы %s: %s", path, exc)
|
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
||||||
return {}
|
return None, str(exc)
|
||||||
if not isinstance(raw, dict):
|
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] = {}
|
out: dict[str, DiagramScheme] = {}
|
||||||
for key, val in raw.items():
|
for key, val in raw.items():
|
||||||
sid = str(key).lower().strip()
|
sid = str(key).lower().strip()
|
||||||
@@ -212,16 +198,70 @@ def _load_schemes_file(path: Path) -> dict[str, DiagramScheme]:
|
|||||||
return out
|
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]:
|
def load_schemes_from_path(path: Path) -> dict[str, DiagramScheme]:
|
||||||
return _load_schemes_file(path)
|
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()
|
dest = path or user_schemes_path()
|
||||||
payload = _schemes_payload(schemes)
|
|
||||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
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(
|
dest.write_text(
|
||||||
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
json.dumps(raw, ensure_ascii=False, indent=2) + "\n",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
return dest
|
return dest
|
||||||
@@ -234,7 +274,7 @@ def claim_user_scheme_if_diverged(
|
|||||||
) -> DiagramScheme:
|
) -> DiagramScheme:
|
||||||
"""
|
"""
|
||||||
If scheme id is in the app template and content differs while still
|
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(
|
stock = (bundled if bundled is not None else _load_schemes_file(bundled_schemes_path())).get(
|
||||||
scheme.id
|
scheme.id
|
||||||
|
|||||||
+69
-17
@@ -16,7 +16,7 @@ from .diagram_schemes import (
|
|||||||
claim_user_scheme_if_diverged,
|
claim_user_scheme_if_diverged,
|
||||||
ensure_user_schemes,
|
ensure_user_schemes,
|
||||||
get_schemes,
|
get_schemes,
|
||||||
load_schemes_from_path,
|
load_schemes_report,
|
||||||
reload_schemes,
|
reload_schemes,
|
||||||
save_schemes,
|
save_schemes,
|
||||||
user_schemes_path,
|
user_schemes_path,
|
||||||
@@ -263,7 +263,9 @@ class Md2GostApp:
|
|||||||
self._last_md = ""
|
self._last_md = ""
|
||||||
self._schemes: dict[str, DiagramScheme] = {}
|
self._schemes: dict[str, DiagramScheme] = {}
|
||||||
self._scheme_ids: list[str] = []
|
self._scheme_ids: list[str] = []
|
||||||
|
self._scheme_file_keys: set[str] = set()
|
||||||
self._scheme_selecting = False
|
self._scheme_selecting = False
|
||||||
|
self._schemes_unreadable: str | None = None
|
||||||
self._debug_next = False
|
self._debug_next = False
|
||||||
|
|
||||||
self._win_files: tk.Toplevel | None = None
|
self._win_files: tk.Toplevel | None = None
|
||||||
@@ -1336,12 +1338,23 @@ class Md2GostApp:
|
|||||||
if not hasattr(self, "_scheme_list"):
|
if not hasattr(self, "_scheme_list"):
|
||||||
return
|
return
|
||||||
path = ensure_user_schemes()
|
path = ensure_user_schemes()
|
||||||
self._schemes = load_schemes_from_path(path)
|
self._schemes, err = load_schemes_report(path)
|
||||||
|
self._schemes_unreadable = err
|
||||||
|
self._scheme_file_keys = set() if err else set(self._schemes)
|
||||||
self._scheme_ids = sorted(self._schemes.keys())
|
self._scheme_ids = sorted(self._schemes.keys())
|
||||||
self._scheme_list.delete(0, tk.END)
|
self._scheme_list.delete(0, tk.END)
|
||||||
for sid in self._scheme_ids:
|
for sid in self._scheme_ids:
|
||||||
title = self._schemes[sid].title or sid
|
title = self._schemes[sid].title or sid
|
||||||
self._scheme_list.insert(tk.END, title)
|
self._scheme_list.insert(tk.END, title)
|
||||||
|
if err:
|
||||||
|
self._clear_scheme_form()
|
||||||
|
self._scheme_fence_hint.configure(
|
||||||
|
text=f"Файл не разобран и не изменён: {err}"
|
||||||
|
)
|
||||||
|
self._append_log(
|
||||||
|
f"Файл схем не прочитан, он не перезаписан: {path} ({err})"
|
||||||
|
)
|
||||||
|
return
|
||||||
if not self._scheme_ids:
|
if not self._scheme_ids:
|
||||||
self._clear_scheme_form()
|
self._clear_scheme_form()
|
||||||
return
|
return
|
||||||
@@ -1352,6 +1365,22 @@ class Md2GostApp:
|
|||||||
self._scheme_list.see(idx)
|
self._scheme_list.see(idx)
|
||||||
self._fill_scheme_form(self._schemes[target])
|
self._fill_scheme_form(self._schemes[target])
|
||||||
|
|
||||||
|
def _persist_schemes(self, select_id: str | None = None) -> str | None:
|
||||||
|
"""Write self._schemes to disk. Refuses to overwrite a file that did not parse."""
|
||||||
|
if self._schemes_unreadable:
|
||||||
|
messagebox.showerror(
|
||||||
|
"md2gost",
|
||||||
|
"Файл схем не разобран, поэтому он не перезаписан.\n"
|
||||||
|
f"{self._schemes_unreadable}",
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
path = user_schemes_path()
|
||||||
|
removed = set(self._scheme_file_keys) - set(self._schemes)
|
||||||
|
save_schemes(self._schemes, path, removed_ids=removed)
|
||||||
|
reload_schemes()
|
||||||
|
self._reload_schemes_ui(select_id=select_id)
|
||||||
|
return str(path)
|
||||||
|
|
||||||
def _clear_scheme_form(self) -> None:
|
def _clear_scheme_form(self) -> None:
|
||||||
self._scheme_selecting = True
|
self._scheme_selecting = True
|
||||||
self._sch_id.set("")
|
self._sch_id.set("")
|
||||||
@@ -1396,6 +1425,9 @@ class Md2GostApp:
|
|||||||
return None
|
return None
|
||||||
includes_raw = self._text_get(self._sch_includes)
|
includes_raw = self._text_get(self._sch_includes)
|
||||||
includes = [ln.strip() for ln in includes_raw.splitlines() if ln.strip()]
|
includes = [ln.strip() for ln in includes_raw.splitlines() if ln.strip()]
|
||||||
|
theme = ""
|
||||||
|
if old_id and old_id in self._schemes:
|
||||||
|
theme = self._schemes[old_id].theme
|
||||||
return DiagramScheme(
|
return DiagramScheme(
|
||||||
id=sid,
|
id=sid,
|
||||||
title=self._sch_title.get().strip() or sid,
|
title=self._sch_title.get().strip() or sid,
|
||||||
@@ -1406,6 +1438,7 @@ class Md2GostApp:
|
|||||||
includes=includes,
|
includes=includes,
|
||||||
prefix=self._text_get(self._sch_prefix),
|
prefix=self._text_get(self._sch_prefix),
|
||||||
postfix=self._text_get(self._sch_postfix),
|
postfix=self._text_get(self._sch_postfix),
|
||||||
|
theme=theme,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _scheme_commit_current(self) -> str | None:
|
def _scheme_commit_current(self) -> str | None:
|
||||||
@@ -1422,6 +1455,15 @@ class Md2GostApp:
|
|||||||
return scheme.id
|
return scheme.id
|
||||||
|
|
||||||
def _scheme_add(self) -> None:
|
def _scheme_add(self) -> None:
|
||||||
|
if self._schemes_unreadable:
|
||||||
|
messagebox.showerror(
|
||||||
|
"md2gost",
|
||||||
|
"Файл схем не разобран, новая схема не записана.\n"
|
||||||
|
f"{self._schemes_unreadable}",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if self._scheme_ids and self._scheme_commit_current() is None:
|
||||||
|
return
|
||||||
n = 1
|
n = 1
|
||||||
while f"custom{n}" in self._schemes:
|
while f"custom{n}" in self._schemes:
|
||||||
n += 1
|
n += 1
|
||||||
@@ -1437,13 +1479,24 @@ class Md2GostApp:
|
|||||||
prefix="@startuml\n",
|
prefix="@startuml\n",
|
||||||
postfix="\n@enduml",
|
postfix="\n@enduml",
|
||||||
)
|
)
|
||||||
self._reload_schemes_ui(select_id=sid)
|
path = self._persist_schemes(select_id=sid)
|
||||||
|
if path:
|
||||||
|
self._append_log(f"Схема добавлена: {sid} ({path})")
|
||||||
|
|
||||||
def _scheme_dup(self) -> None:
|
def _scheme_dup(self) -> None:
|
||||||
sel = self._scheme_list.curselection()
|
if not self._scheme_list.curselection():
|
||||||
if not sel:
|
|
||||||
return
|
return
|
||||||
src = self._schemes[self._scheme_ids[sel[0]]]
|
if self._schemes_unreadable:
|
||||||
|
messagebox.showerror(
|
||||||
|
"md2gost",
|
||||||
|
"Файл схем не разобран, копия не записана.\n"
|
||||||
|
f"{self._schemes_unreadable}",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
src_id = self._scheme_commit_current()
|
||||||
|
if src_id is None:
|
||||||
|
return
|
||||||
|
src = self._schemes[src_id]
|
||||||
n = 1
|
n = 1
|
||||||
while f"{src.id}_copy{n}" in self._schemes:
|
while f"{src.id}_copy{n}" in self._schemes:
|
||||||
n += 1
|
n += 1
|
||||||
@@ -1452,7 +1505,7 @@ class Md2GostApp:
|
|||||||
id=sid,
|
id=sid,
|
||||||
title=src.title,
|
title=src.title,
|
||||||
version=src.version,
|
version=src.version,
|
||||||
author=src.author,
|
author="",
|
||||||
docs=src.docs,
|
docs=src.docs,
|
||||||
ai_prompt=src.ai_prompt,
|
ai_prompt=src.ai_prompt,
|
||||||
includes=list(src.includes),
|
includes=list(src.includes),
|
||||||
@@ -1460,7 +1513,9 @@ class Md2GostApp:
|
|||||||
postfix=src.postfix,
|
postfix=src.postfix,
|
||||||
theme=src.theme,
|
theme=src.theme,
|
||||||
)
|
)
|
||||||
self._reload_schemes_ui(select_id=sid)
|
path = self._persist_schemes(select_id=sid)
|
||||||
|
if path:
|
||||||
|
self._append_log(f"Схема скопирована: {sid} ({path})")
|
||||||
|
|
||||||
def _scheme_del(self) -> None:
|
def _scheme_del(self) -> None:
|
||||||
sel = self._scheme_list.curselection()
|
sel = self._scheme_list.curselection()
|
||||||
@@ -1470,20 +1525,17 @@ class Md2GostApp:
|
|||||||
if not messagebox.askyesno("md2gost", f"Удалить схему «{sid}» из файла пользователя?"):
|
if not messagebox.askyesno("md2gost", f"Удалить схему «{sid}» из файла пользователя?"):
|
||||||
return
|
return
|
||||||
del self._schemes[sid]
|
del self._schemes[sid]
|
||||||
path = user_schemes_path()
|
path = self._persist_schemes()
|
||||||
save_schemes(self._schemes, path)
|
if path:
|
||||||
reload_schemes()
|
self._append_log(f"Схема удалена: {sid}")
|
||||||
self._reload_schemes_ui()
|
|
||||||
self._append_log(f"Схема удалена: {sid}")
|
|
||||||
|
|
||||||
def _scheme_save_all(self) -> None:
|
def _scheme_save_all(self) -> None:
|
||||||
sid = self._scheme_commit_current()
|
sid = self._scheme_commit_current()
|
||||||
if sid is None and self._scheme_ids:
|
if sid is None and self._scheme_ids:
|
||||||
return
|
return
|
||||||
path = user_schemes_path()
|
path = self._persist_schemes(select_id=sid)
|
||||||
save_schemes(self._schemes, path)
|
if not path:
|
||||||
reload_schemes()
|
return
|
||||||
self._reload_schemes_ui(select_id=sid)
|
|
||||||
self._append_log(f"Схемы сохранены: {path}")
|
self._append_log(f"Схемы сохранены: {path}")
|
||||||
messagebox.showinfo("md2gost", f"Сохранено:\n{path}")
|
messagebox.showinfo("md2gost", f"Сохранено:\n{path}")
|
||||||
|
|
||||||
|
|||||||
@@ -205,11 +205,11 @@ SCHEMES_HELP = """СХЕМЫ ДИАГРАММ (PlantUML)
|
|||||||
Первый запуск
|
Первый запуск
|
||||||
Рядом с программой (рядом с md2gost.exe или в текущей папке при python -m)
|
Рядом с программой (рядом с md2gost.exe или в текущей папке при python -m)
|
||||||
создаётся md2gost.schemes.json из встроенного шаблона.
|
создаётся md2gost.schemes.json из встроенного шаблона.
|
||||||
При каждом запуске схемы с author=md2gost синхронизируются с шаблоном приложения
|
При каждом запуске в файл добавляются только те встроенные схемы, которых ещё нет.
|
||||||
(добавление новых id, обновление, удаление исчезнувших вроде bpmn).
|
Уже записанные схемы не перезаписываются и не удаляются: правки и свои шаблоны
|
||||||
Схемы с пустым или другим author не трогаются.
|
сохраняются. Файл с ошибкой JSON или с BOM не затирается.
|
||||||
Если правите встроенную схему в GUI и сохраняете — author сбрасывается,
|
Если правите встроенную схему в GUI и сохраняете — author сбрасывается,
|
||||||
чтобы следующая миграция не затёрла ваши правки.
|
схема помечается как ваша.
|
||||||
|
|
||||||
Оградка в markdown
|
Оградка в markdown
|
||||||
```uml — обычный PlantUML (или ```plantuml)
|
```uml — обычный PlantUML (или ```plantuml)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ from md2gost.diagram_schemes import (
|
|||||||
ensure_user_schemes,
|
ensure_user_schemes,
|
||||||
load_schemes_from_path,
|
load_schemes_from_path,
|
||||||
migrate_user_schemes,
|
migrate_user_schemes,
|
||||||
|
save_schemes,
|
||||||
prepare_with_schemes,
|
prepare_with_schemes,
|
||||||
scheme_id_from_lang,
|
scheme_id_from_lang,
|
||||||
user_schemes_path,
|
user_schemes_path,
|
||||||
@@ -125,7 +126,7 @@ def test_ensure_user_schemes_once(tmp_path):
|
|||||||
assert path == user_schemes_path(tmp_path)
|
assert path == user_schemes_path(tmp_path)
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_removes_orphaned_builtin_and_adds_missing(tmp_path, monkeypatch):
|
def test_migrate_keeps_custom_schemes_and_adds_missing(tmp_path, monkeypatch):
|
||||||
monkeypatch.chdir(tmp_path)
|
monkeypatch.chdir(tmp_path)
|
||||||
path = user_schemes_path(tmp_path)
|
path = user_schemes_path(tmp_path)
|
||||||
path.write_text(
|
path.write_text(
|
||||||
@@ -147,10 +148,11 @@ def test_migrate_removes_orphaned_builtin_and_adds_missing(tmp_path, monkeypatch
|
|||||||
)
|
)
|
||||||
result = migrate_user_schemes(tmp_path)
|
result = migrate_user_schemes(tmp_path)
|
||||||
assert result.written
|
assert result.written
|
||||||
assert "bpmn" in result.removed
|
assert result.removed == []
|
||||||
assert "c4" in result.added
|
assert "c4" in result.added
|
||||||
schemes = load_schemes_from_path(path)
|
schemes = load_schemes_from_path(path)
|
||||||
assert "bpmn" not in schemes
|
assert schemes["bpmn"].title == "BPMN 2.0"
|
||||||
|
assert schemes["bpmn"].author == "md2gost"
|
||||||
assert "mine" in schemes
|
assert "mine" in schemes
|
||||||
assert schemes["mine"].title == "x"
|
assert schemes["mine"].title == "x"
|
||||||
bundled = load_schemes_from_path(bundled_schemes_path())
|
bundled = load_schemes_from_path(bundled_schemes_path())
|
||||||
@@ -186,7 +188,7 @@ def test_migrate_preserves_user_override_of_builtin(tmp_path, monkeypatch):
|
|||||||
assert "usecase" in schemes # other bundled ids still added
|
assert "usecase" in schemes # other bundled ids still added
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_updates_app_owned_builtin(tmp_path, monkeypatch):
|
def test_migrate_keeps_edited_builtin(tmp_path, monkeypatch):
|
||||||
monkeypatch.chdir(tmp_path)
|
monkeypatch.chdir(tmp_path)
|
||||||
path = user_schemes_path(tmp_path)
|
path = user_schemes_path(tmp_path)
|
||||||
path.write_text(
|
path.write_text(
|
||||||
@@ -208,10 +210,83 @@ def test_migrate_updates_app_owned_builtin(tmp_path, monkeypatch):
|
|||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
result = migrate_user_schemes(tmp_path)
|
result = migrate_user_schemes(tmp_path)
|
||||||
assert "c4" in result.updated
|
assert "c4" not in result.updated
|
||||||
|
assert "c4" not in result.added
|
||||||
schemes = load_schemes_from_path(path)
|
schemes = load_schemes_from_path(path)
|
||||||
bundled = load_schemes_from_path(bundled_schemes_path())
|
assert schemes["c4"].title == "Old C4"
|
||||||
assert schemes["c4"].to_dict() == bundled["c4"].to_dict()
|
assert schemes["c4"].docs == "stale"
|
||||||
|
assert schemes["c4"].author == "md2gost"
|
||||||
|
assert "usecase" in schemes
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_does_not_overwrite_broken_json(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
path = user_schemes_path(tmp_path)
|
||||||
|
original = "{\n \"c4\": {\"title\": \"x\",}\n}\n"
|
||||||
|
path.write_text(original, encoding="utf-8")
|
||||||
|
result = migrate_user_schemes(tmp_path)
|
||||||
|
assert not result.written
|
||||||
|
assert not result.changed
|
||||||
|
assert path.read_text(encoding="utf-8") == original
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_reads_utf8_bom(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
path = user_schemes_path(tmp_path)
|
||||||
|
body = json.dumps(
|
||||||
|
{
|
||||||
|
"c4": {
|
||||||
|
"title": "My C4",
|
||||||
|
"author": "md2gost",
|
||||||
|
"docs": "mine",
|
||||||
|
"prefix": "",
|
||||||
|
"postfix": "",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
path.write_bytes(b"\xef\xbb\xbf" + body.encode("utf-8"))
|
||||||
|
result = migrate_user_schemes(tmp_path)
|
||||||
|
assert "usecase" in result.added
|
||||||
|
schemes = load_schemes_from_path(path)
|
||||||
|
assert schemes["c4"].title == "My C4"
|
||||||
|
assert schemes["c4"].docs == "mine"
|
||||||
|
text = path.read_text(encoding="utf-8-sig")
|
||||||
|
assert "mine" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_schemes_keeps_unknown_entries(tmp_path):
|
||||||
|
path = user_schemes_path(tmp_path)
|
||||||
|
path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"my-scheme": {"title": "hyphen", "note": "keep"},
|
||||||
|
"mine": {"title": "x", "author": "", "extra": "field", "prefix": "A"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
+ "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
loaded = load_schemes_from_path(path)
|
||||||
|
assert "mine" in loaded
|
||||||
|
assert "my-scheme" not in loaded
|
||||||
|
loaded["mine"] = DiagramScheme(
|
||||||
|
id="mine",
|
||||||
|
title="y",
|
||||||
|
author="",
|
||||||
|
prefix="A",
|
||||||
|
)
|
||||||
|
save_schemes(loaded, path, removed_ids=set())
|
||||||
|
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
assert raw["my-scheme"]["title"] == "hyphen"
|
||||||
|
assert raw["my-scheme"]["note"] == "keep"
|
||||||
|
assert raw["mine"]["title"] == "y"
|
||||||
|
assert raw["mine"]["extra"] == "field"
|
||||||
|
del loaded["mine"]
|
||||||
|
save_schemes(loaded, path, removed_ids={"mine"})
|
||||||
|
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
assert "mine" not in raw
|
||||||
|
assert raw["my-scheme"]["note"] == "keep"
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_idempotent(tmp_path, monkeypatch):
|
def test_migrate_idempotent(tmp_path, monkeypatch):
|
||||||
|
|||||||
Reference in New Issue
Block a user