fix(legacy): repoint a stored dictionary path even when no file was copied
A stored dictionary path to an importer-owned legacy file was rewritten only when this run copied that file. A user who never created a legacy dictionary, or who already has a new dictionary.txt, kept the legacy path, and the first edit was written back into the legacy directory. The path is now rewritten to the file's new location whenever it names a payload file, copied this run or not. Paths outside the legacy payload are kept as stored. Refs #18
This commit is contained in:
@@ -9,9 +9,11 @@ always wins: no merging, no write-back to the legacy location.
|
||||
Exactly-once falls out of existence checks — a copied file is never copied
|
||||
again, and a run that already has new locations imports nothing.
|
||||
|
||||
A stored custom-dictionary path that names a legacy file copied in this run is
|
||||
rewritten in the copied configuration to the new location, so edits never land
|
||||
in the legacy file. Any other path (a location the user chose elsewhere) is kept.
|
||||
A stored custom-dictionary path that names an importer-owned legacy file (one of
|
||||
the payload files above) is rewritten in the copied configuration to that file's
|
||||
new location, whether or not this run copied it. Edits then never land in the
|
||||
legacy file. Any other path, including a user-chosen file elsewhere or another
|
||||
file inside the legacy directory, is kept as stored.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -83,20 +85,20 @@ def _copy_once(src: str, dst: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _copied_destination(stored: str, copied: dict[str, str]) -> Optional[str]:
|
||||
"""Return the new location of the copied legacy file ``stored`` names, if any."""
|
||||
def _payload_destination(stored: str, payload: dict[str, str]) -> Optional[str]:
|
||||
"""Return the new location of the importer-owned legacy file ``stored`` names."""
|
||||
try:
|
||||
target = os.path.realpath(os.path.expanduser(stored))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
for source, destination in copied.items():
|
||||
for source, destination in payload.items():
|
||||
if target == os.path.realpath(source):
|
||||
return destination
|
||||
return None
|
||||
|
||||
|
||||
def _repoint_dictionary_path(config_file: str, copied: dict[str, str]) -> None:
|
||||
"""Rewrite the copied config's dictionary path when it names a copied legacy file.
|
||||
def _repoint_dictionary_path(config_file: str, payload: dict[str, str]) -> None:
|
||||
"""Rewrite the copied config's dictionary path when it names a payload file.
|
||||
|
||||
Only the new configuration copy is written; the legacy file is only read.
|
||||
"""
|
||||
@@ -112,7 +114,7 @@ def _repoint_dictionary_path(config_file: str, copied: dict[str, str]) -> None:
|
||||
stored = dictionary.get("file_path")
|
||||
if not isinstance(stored, str) or not stored.strip():
|
||||
return
|
||||
destination = _copied_destination(stored, copied)
|
||||
destination = _payload_destination(stored, payload)
|
||||
if destination is None:
|
||||
return
|
||||
dictionary["file_path"] = destination
|
||||
@@ -146,24 +148,25 @@ def import_legacy_once() -> list[str]:
|
||||
return []
|
||||
|
||||
imported: list[str] = []
|
||||
copied: dict[str, str] = {}
|
||||
# Every importer-owned legacy file and its new location, copied this run or
|
||||
# not: a stored path to one of them must never resolve to the legacy file.
|
||||
payload: dict[str, str] = {}
|
||||
for filename in CONFIG_PAYLOAD:
|
||||
src = os.path.join(legacy_config, filename)
|
||||
dst = os.path.join(new_config_dir(), filename)
|
||||
payload[src] = dst
|
||||
if _copy_once(src, dst):
|
||||
imported.append(dst)
|
||||
copied[src] = dst
|
||||
legacy_data = legacy_data_dir()
|
||||
if os.path.isdir(legacy_data):
|
||||
for filename in DATA_PAYLOAD:
|
||||
src = os.path.join(legacy_data, filename)
|
||||
dst = os.path.join(new_data_dir(), filename)
|
||||
if _copy_once(src, dst):
|
||||
imported.append(dst)
|
||||
copied[src] = dst
|
||||
for filename in DATA_PAYLOAD:
|
||||
src = os.path.join(legacy_data, filename)
|
||||
dst = os.path.join(new_data_dir(), filename)
|
||||
payload[src] = dst
|
||||
if os.path.isdir(legacy_data) and _copy_once(src, dst):
|
||||
imported.append(dst)
|
||||
copied_config = os.path.join(new_config_dir(), CONFIG_FILENAME)
|
||||
if copied_config in imported:
|
||||
_repoint_dictionary_path(copied_config, copied)
|
||||
_repoint_dictionary_path(copied_config, payload)
|
||||
if imported:
|
||||
logger.info("Legacy Vocalinux import complete: %d file(s)", len(imported))
|
||||
return imported
|
||||
|
||||
@@ -169,13 +169,25 @@ def _import_with_stored_dictionary_path(
|
||||
encoding="utf-8",
|
||||
)
|
||||
import_legacy_once()
|
||||
_point_config_manager_at(monkeypatch, config_root / "voculinux")
|
||||
return seeded["dictionary.txt"]
|
||||
|
||||
new_config_dir = config_root / "voculinux"
|
||||
|
||||
def _point_config_manager_at(monkeypatch, new_config_dir: Path) -> None:
|
||||
"""Make ConfigManager read and write only the given (scratch) configuration dir."""
|
||||
from vocalinux.ui import config_manager
|
||||
|
||||
monkeypatch.setattr(config_manager, "CONFIG_DIR", str(new_config_dir))
|
||||
monkeypatch.setattr(config_manager, "CONFIG_FILE", str(new_config_dir / "config.json"))
|
||||
return seeded["dictionary.txt"]
|
||||
|
||||
|
||||
def _add_term_through_migrated_config(monkeypatch, new_config_dir: Path, term: str) -> None:
|
||||
"""Add a dictionary term exactly as the app does, using the migrated configuration."""
|
||||
_point_config_manager_at(monkeypatch, new_config_dir)
|
||||
from vocalinux.custom_dictionary import CustomDictionaryManager
|
||||
from vocalinux.ui.config_manager import ConfigManager
|
||||
|
||||
assert CustomDictionaryManager(ConfigManager()).add_term(term) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("form", ["absolute-legacy", "tilde-legacy"])
|
||||
@@ -221,3 +233,47 @@ def test_dictionary_path_outside_the_legacy_directory_is_kept(monkeypatch, tmp_p
|
||||
migrated = json.loads((config_root / "voculinux" / "config.json").read_text(encoding="utf-8"))
|
||||
assert migrated["dictionary"]["file_path"] == str(chosen)
|
||||
assert chosen.read_text(encoding="utf-8") == "Chosen\n"
|
||||
|
||||
|
||||
def test_absent_legacy_dictionary_edit_lands_in_the_new_file(monkeypatch, tmp_path) -> None:
|
||||
"""The default dictionary path was saved but the file was never created."""
|
||||
config_root, _data_root = _isolate(monkeypatch, tmp_path)
|
||||
legacy_config = config_root / "vocalinux"
|
||||
legacy_config.mkdir(parents=True)
|
||||
legacy_terms = legacy_config / "dictionary.txt"
|
||||
(legacy_config / "config.json").write_text(
|
||||
json.dumps({"dictionary": {"enabled": True, "file_path": str(legacy_terms)}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
import_legacy_once()
|
||||
|
||||
_add_term_through_migrated_config(monkeypatch, config_root / "voculinux", "Kubernetes")
|
||||
|
||||
assert not legacy_terms.exists()
|
||||
assert "Kubernetes" in (config_root / "voculinux" / "dictionary.txt").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
def test_existing_new_dictionary_takes_the_edit_and_legacy_stays_put(monkeypatch, tmp_path) -> None:
|
||||
"""A new dictionary already present wins; the legacy file is never written."""
|
||||
config_root, data_root = _isolate(monkeypatch, tmp_path)
|
||||
seeded = _seed_legacy(config_root, data_root)
|
||||
legacy_terms = seeded["dictionary.txt"]
|
||||
seeded["config.json"].write_text(
|
||||
json.dumps({"dictionary": {"enabled": True, "file_path": str(legacy_terms)}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
new_config_dir = config_root / "voculinux"
|
||||
new_config_dir.mkdir(parents=True)
|
||||
(new_config_dir / "dictionary.txt").write_text("Mine\n", encoding="utf-8")
|
||||
os.utime(legacy_terms, ns=(_OLD_MTIME_NS, _OLD_MTIME_NS))
|
||||
legacy_bytes = legacy_terms.read_bytes()
|
||||
legacy_mtime = legacy_terms.stat().st_mtime_ns
|
||||
import_legacy_once()
|
||||
|
||||
_add_term_through_migrated_config(monkeypatch, new_config_dir, "Kubernetes")
|
||||
|
||||
assert legacy_terms.read_bytes() == legacy_bytes
|
||||
assert legacy_terms.stat().st_mtime_ns == legacy_mtime
|
||||
assert (new_config_dir / "dictionary.txt").read_text(encoding="utf-8") == "Mine\nKubernetes\n"
|
||||
|
||||
Reference in New Issue
Block a user