fix: write scraped files whole or not at all

This commit is contained in:
Abdessamad Derraz committed 2026-10-10 04:45:10 +02:00
1 parent 170b7d93ae
commit a00f57ad37
7 files changed
+51 -19

No files matched your search

+1 -1
View File
@@ -60,7 +60,7 @@ def write_text_atomic(path: str, content: str) -> None:
dir=directory, prefix=f".{os.path.basename(path)}.", suffix=".tmp"
)
try:
with os.fdopen(handle, "w") as f:
with os.fdopen(handle, "w", encoding="utf-8") as f:
f.write(content)
os.replace(scratch, path)
except BaseException:
+3 -3
View File
@@ -14,7 +14,7 @@ from pathlib import Path
from typing import Any
from common import yaml_load
from common import write_text_atomic, yaml_load
_MAME_RELEASE_RE = re.compile(r"^0\.\d+")
@@ -390,7 +390,7 @@ def _backup_and_write(path: str, data: dict) -> None:
patched = _patch_bios_entries(patched, data.get("files", []))
patched = _append_new_entries(patched, data.get("files", []), original)
p.write_text(patched, encoding="utf-8")
write_text_atomic(str(p), patched)
def _patch_core_version(text: str, version: str) -> str:
@@ -606,4 +606,4 @@ def _backup_and_write_fbneo(path: str, data: dict, hashes: dict) -> None:
lines.append("")
patched = patched.rstrip("\n") + "\n\n" + "\n".join(lines)
p.write_text(patched, encoding="utf-8")
write_text_atomic(str(p), patched)
+7 -6
View File
@@ -12,7 +12,7 @@ import yaml
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from pathlib import Path
from common import yaml_load
from common import write_text_atomic, yaml_load
@dataclass
@@ -373,11 +373,12 @@ def scraper_cli(
for field in ("data_directories",):
if field in old_sys and field not in sys_data:
sys_data[field] = old_sys[field]
with open(output_path, "w") as f:
yaml.dump(
config, f, Dumper=_PlatformDumper,
default_flow_style=False, allow_unicode=True, sort_keys=False,
)
# Written whole or not at all: a reader in the middle of a streamed
# dump parsed a valid platform with a fraction of its systems.
write_text_atomic(output_path, yaml.dump(
config, Dumper=_PlatformDumper,
default_flow_style=False, allow_unicode=True, sort_keys=False,
))
print(f"Written {len(reqs)} entries to {args.output}")
return
+3 -4
View File
@@ -20,7 +20,7 @@ from typing import Any
import yaml
from common import yaml_load
from common import write_text_atomic, yaml_load
from scripts.scraper._hash_merge import compute_diff, merge_fbneo_profile
from scripts.scraper.fbneo_parser import parse_fbneo_source_tree
@@ -187,9 +187,8 @@ def fetch_and_cache(force: bool = False) -> dict[str, Any]:
}
CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
CACHE_PATH.write_text(
json.dumps(cache, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
write_text_atomic(
str(CACHE_PATH), json.dumps(cache, indent=2, ensure_ascii=False) + "\n"
)
log.info("wrote %d BIOS sets to %s", len(bios_sets), CACHE_PATH)
+4 -3
View File
@@ -22,7 +22,7 @@ from typing import Any
import yaml
from common import yaml_load
from common import write_text_atomic, yaml_load
from ._hash_merge import compute_diff, merge_mame_profile
from .mame_parser import parse_mame_source_tree
@@ -66,8 +66,9 @@ def _is_stale(cache: dict[str, Any] | None) -> bool:
def _write_cache(data: dict[str, Any]) -> None:
_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
with open(_CACHE_PATH, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
write_text_atomic(
str(_CACHE_PATH), json.dumps(data, indent=2, ensure_ascii=False)
)
log.info("cache written to %s", _CACHE_PATH)
+5 -2
View File
@@ -28,8 +28,11 @@ class BaseTargetScraper(ABC):
import yaml
except ImportError:
raise ImportError("PyYAML required: pip install pyyaml")
with open(output_path, "w") as f:
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
from common import write_text_atomic
write_text_atomic(
output_path, yaml.dump(data, default_flow_style=False, sort_keys=False)
)
_scrapers: dict[str, type] = {}
+28
View File
@@ -65,5 +65,33 @@ class EveryWriterGoesThroughIt(unittest.TestCase):
self.assertNotIn("shutil.copy2(source, path)", (scripts / "restore_large_files.py").read_text())
def test_scrapers_write_whole_files(self):
"""A streamed yaml.dump into platforms/<x>.yml let a concurrent reader
parse a valid platform holding a fraction of its systems."""
import re
streamed = re.compile(
r"open\([^)]*['\"](?:w|wb|a)['\"]|\.write_text\(|\.write_bytes\("
)
offenders = [
f"{path.relative_to(REPO_ROOT)}:{number}"
for path in sorted((REPO_ROOT / "scripts" / "scraper").rglob("*.py"))
for number, line in enumerate(
path.read_text(encoding="utf-8").splitlines(), 1
)
if streamed.search(line)
]
self.assertEqual(offenders, [])
def test_the_text_is_utf8_whatever_the_locale(self):
import tempfile
with tempfile.TemporaryDirectory() as directory:
target = Path(directory) / "out.yml"
artifacts.write_text_atomic(str(target), "name: \u00e9mulateur \u2014 \u30d5\n")
self.assertEqual(
target.read_bytes().decode("utf-8"), "name: \u00e9mulateur \u2014 \u30d5\n"
)
if __name__ == "__main__":
unittest.main()