Skip to content

Commit 6b5345c

Browse files
alexmmillerclaude
andcommitted
Fix the seven findings from the fourth /code-review xhigh pass
The drift check's exemption keyed off the wrong thing, and let real drift through. `if src in unpredicted` read as "the planner claimed nothing for this source", but it recorded "the probe failed" - and those come apart under --webp false, where possible_output_names returns the source's own name before it ever consults `animated`. So for any file whose probe failed, a writer that produced a name nobody claimed got a warning and an exit 0 instead of the error the check exists to raise. The warning even said "the de-confliction pass held no name for it" while `claimed` held one. Rather than correct the condition, remove the case: optimize_raster now takes the planner's `animated` answer instead of re-deriving it with its own `getattr(img, "is_animated", False)` a few lines later. Two answers to one question was the whole reason a source could be unpredictable here and processable there. A source whose animation is undetermined is now refused in optimize_raster, counted by process_file as one file's error like any other, and never reaches the check - so the planner's "claims nothing" is always accurate and the exemption has nothing left to describe. Both answers already came from the same access on the same file, so this only changes behaviour on a transient. The per-source plan is one record (Plan: dst_rel, names, animated) instead of a dst dict beside a parallel set - every parallel structure this pass has grown ended up disagreeing with the one next to it, which is what both of the last two rounds' bugs were. Also: - The drift message reports `plan.names`, the claim as the planner wrote it, not `claimed`'s casefolded keys - reporting "diagram.svg" for a source that claimed "Diagram.SVG" points the reader at a case mismatch that isn't the problem. - The check compares with == rather than `is`: Path defines __eq__, and identity held only because two loops happened to share list objects. - The uppercase test asserts the written name per case, not just that one file came out. Dropped test_drift_is_refused_even_when_the_probe_failed as written: it stubbed the probe to False - a probe that succeeded - so it demonstrated nothing about the exemption and passed against the unfixed tree. The regression for that hole is the --webp False half of test_an_undetermined_probe_skips_one_file_and_no_more, which fails against it. 206 + 33 + 189 pass; scripts/ unchanged from base. Real 299-image corpus byte-for-byte identical, and act gives the same database md5 for the fourth round running. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 966fb90 commit 6b5345c

2 files changed

Lines changed: 131 additions & 63 deletions

File tree

‎ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/optimize_media.py‎

Lines changed: 82 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -296,9 +296,28 @@ def resize_animated_gif(img: Image.Image, dst: Path, max_width: int) -> Path:
296296
return dst
297297

298298

299-
def optimize_raster(src: Path, dst: Path, **encode_kwargs) -> Path:
299+
def optimize_raster(src: Path, dst: Path, *, animated: Optional[bool], **encode_kwargs) -> Path:
300+
"""`animated` is decided once, by _is_animated_raster during the
301+
name-planning pass, and passed in rather than re-derived here.
302+
303+
This used to repeat the planner's `getattr(img, "is_animated", False)`
304+
a few lines after the planner ran it, which is two answers to one
305+
question - and the possibility of them differing is the whole reason
306+
optimize_directory had to carry a set of sources it had "declined to
307+
predict" and special-case them when checking what got written. Both
308+
answers came from the same access on the same file, so in practice they
309+
could only differ on a transient (the file changed or was locked between
310+
the two opens); taking the planner's answer as authoritative removes the
311+
case rather than handling it.
312+
313+
None means the planner could not tell. It refuses rather than guessing:
314+
the planner has already recorded that this source is expected to produce
315+
nothing, and process_file turns this into one file's counted error."""
316+
if animated is None:
317+
raise ValueError("could not determine whether this file is animated (see the earlier warning); "
318+
"skipping it rather than guessing at its output format")
300319
with Image.open(src) as img:
301-
if getattr(img, "is_animated", False):
320+
if animated:
302321
if src.suffix.lower() == ".gif":
303322
return resize_animated_gif(img, dst, encode_kwargs["max_width"])
304323
# Animated non-GIF (e.g. webp): per-frame resizing/re-encoding is
@@ -382,12 +401,19 @@ def optimize_svg(src: Path, dst: Path, *, precision: int, rasterize_threshold: i
382401
return dst, False
383402

384403

385-
def process_file(src: Path, dst: Path, *, cfg: dict, pngquant_path: str, stats: dict, logger: Logger) -> Path:
404+
def process_file(src: Path, dst: Path, *, cfg: dict, pngquant_path: str, stats: dict, logger: Logger,
405+
animated: Optional[bool]) -> Path:
386406
"""Optimizes (or copies through) one file. Returns the path actually
387407
written on success (which may differ from `dst` - webp conversion or
388408
SVG rasterization changes the extension), or None on error (already
389409
logged; `stats["errors"]` is incremented so callers can tell without
390-
inspecting the return value)."""
410+
inspecting the return value).
411+
412+
`animated` is required, not defaulted: it is the name-planning pass's
413+
answer for this source (see optimize_raster), and a default would let a
414+
caller silently skip the planning this function's output is checked
415+
against. Irrelevant for SVG and passthrough files, which have no
416+
animation to honour."""
391417
dst.parent.mkdir(parents=True, exist_ok=True)
392418
suffix = src.suffix.lower()
393419

@@ -410,7 +436,8 @@ def process_file(src: Path, dst: Path, *, cfg: dict, pngquant_path: str, stats:
410436
kind = "svg"
411437
elif suffix in RASTER_EXTENSIONS:
412438
dst_final = optimize_raster(
413-
src, dst, max_width=cfg["max_width"], jpeg_quality=cfg["jpeg_quality"], webp=cfg["webp"],
439+
src, dst, animated=animated,
440+
max_width=cfg["max_width"], jpeg_quality=cfg["jpeg_quality"], webp=cfg["webp"],
414441
webp_quality=cfg["webp_quality"], pngquant_path=pngquant_path, pngquant_speed=cfg["pngquant_speed"],
415442
logger=logger,
416443
)
@@ -446,6 +473,19 @@ def process_file(src: Path, dst: Path, *, cfg: dict, pngquant_path: str, stats:
446473
return dst_final
447474

448475

476+
class Plan(NamedTuple):
477+
"""What the name-planning pass decided for one source: where it will be
478+
written, every basename it holds against the other sources, and the
479+
animation answer process_file is to use. One record rather than a dict
480+
per field - the three are decided together, in one loop, and every
481+
parallel structure this pass has grown so far ended up disagreeing with
482+
the one beside it."""
483+
484+
dst_rel: Path
485+
names: set
486+
animated: Optional[bool]
487+
488+
449489
class OptimizeResult(NamedTuple):
450490
"""What optimize_directory did.
451491
@@ -666,18 +706,13 @@ def optimize_directory(input_dir: Path, output_dir: Path, *, cfg: dict, pngquant
666706
deduped.append(src)
667707
sources = deduped
668708

669-
dst_rel_for = {}
709+
plan_for = {}
670710
# Keyed casefolded: the work dir is written on whatever filesystem the run
671711
# happens to use, and on a case-insensitive one (APFS, NTFS) "logo.PNG" and
672712
# "logo.png" are the same file - so comparing the names as written would
673713
# miss a collision that still costs an image. insert_optimized_media's own
674714
# seen_names guard is case-sensitive too and would not catch it either.
675715
claimed = {}
676-
# Sources whose animation could not be determined, and which therefore
677-
# claimed no names at all (see possible_output_names). Kept so the check
678-
# below can tell "the planner and the writer disagree" - a bug - from
679-
# "the planner declined to predict this one" - one file's bad luck.
680-
unpredicted = set()
681716
for src in sources:
682717
rel = src.relative_to(input_dir)
683718
stem, suffix = rel.stem, rel.suffix
@@ -687,8 +722,6 @@ def optimize_directory(input_dir: Path, output_dir: Path, *, cfg: dict, pngquant
687722
# cannot carry a second frame is never animated, and opening every
688723
# JPEG to learn that would cost an extra pass over the whole corpus.
689724
animated = _is_animated_raster(src, logger) if suffix.lower() in ANIMATABLE_EXTENSIONS else False
690-
if animated is None:
691-
unpredicted.add(src)
692725
candidate = stem
693726
attempt = 0
694727
while True:
@@ -706,14 +739,16 @@ def optimize_directory(input_dir: Path, output_dir: Path, *, cfg: dict, pngquant
706739
)
707740
for name in names:
708741
claimed[name.lower()] = src
709-
dst_rel_for[src] = rel.parent / f"{candidate}{suffix}"
742+
plan_for[src] = Plan(dst_rel=rel.parent / f"{candidate}{suffix}", names=names, animated=animated)
710743

711744
renamed = {}
712745
written = []
713746
for src in sources:
714747
rel = src.relative_to(input_dir)
715-
dst = output_dir / dst_rel_for[src]
716-
dst_final = process_file(src, dst, cfg=cfg, pngquant_path=pngquant_path, stats=stats, logger=logger)
748+
plan = plan_for[src]
749+
dst = output_dir / plan.dst_rel
750+
dst_final = process_file(src, dst, cfg=cfg, pngquant_path=pngquant_path, stats=stats, logger=logger,
751+
animated=plan.animated)
717752
if dst_final is None:
718753
continue
719754
# The name this actually wrote has to be one the planning pass held
@@ -728,43 +763,39 @@ def optimize_directory(input_dir: Path, output_dir: Path, *, cfg: dict, pngquant
728763
# a name written but never claimed, free to overwrite another source's
729764
# output. Nothing connected the two halves, so nothing noticed.
730765
#
731-
# This is that connection. It costs one dict lookup per file and turns
732-
# a future drift into a named failure instead of a corrupted image
766+
# This is that connection. It costs one lookup per file and turns a
767+
# future drift into a named failure instead of a corrupted image
733768
# corpus that still exits 0.
734769
#
735-
# Asked of `claimed` rather than of a second per-source dict, and so
736-
# casefolded like every other lookup in this pass. De-confliction
737-
# guarantees no two sources hold the same lowercased name, so "is this
738-
# the source that claimed this name" is exactly `claimed[name] is src`.
739-
# Comparing exact case instead made the check stricter than the
740-
# invariant it protects, and turned an SVG spelled ".SVG" - which the
741-
# planner claimed as ".svg" - into an aborted run.
742-
if claimed.get(dst_final.name.lower()) is not src:
743-
held = sorted(name for name, owner in claimed.items() if owner is src)
744-
if src in unpredicted:
745-
# Not drift: the planner declined to predict this one because
746-
# its animation could not be determined, expecting
747-
# optimize_raster to fail on the same access. It didn't. The
748-
# file itself is fine - it is kept rather than dropped, since
749-
# discarding a successfully optimized image is the worse
750-
# outcome and insert_optimized_media's seen_names guard still
751-
# catches a flat-namespace collision downstream - but the
752-
# planning pass never de-conflicted it, so say so.
753-
logger.error(
754-
f"warning: {src} produced {dst_final.name} after its animation could not be determined, "
755-
"so the de-confliction pass held no name for it; keeping the file, but it was not "
756-
"checked against the other sources' outputs"
757-
)
758-
else:
759-
# Genuine drift. ValueError, not RuntimeError: that is what
760-
# insert_optimized_media's `except ValueError` around this call
761-
# turns into a clean "error: ..." line in the log file, the
762-
# same as this function's input_dir == output_dir refusal.
763-
raise ValueError(
764-
f"{src} wrote {dst_final.name}, which possible_output_names did not predict "
765-
f"(it claimed {held or 'nothing'}). The output-name planning pass and process_file "
766-
"have drifted apart - see possible_output_names."
767-
)
770+
# Asked of `claimed`, and so casefolded like every other lookup in
771+
# this pass: de-confliction guarantees no two sources hold the same
772+
# lowercased name, so "is this the source that claimed this name" is
773+
# exactly `claimed[name] == src`. Comparing exact case instead made
774+
# the check stricter than the invariant it protects, and turned an SVG
775+
# spelled ".SVG" - which the planner claimed as ".svg" - into an
776+
# aborted run.
777+
#
778+
# No exemption for a source the planner could not predict: it does not
779+
# arise any more. optimize_raster takes the planner's answer instead
780+
# of re-deriving it, so a source whose animation was undetermined
781+
# raises there, is counted as one file's error, and never reaches
782+
# here. The exemption that used to sit in this branch keyed off "the
783+
# probe failed" rather than "the planner claimed nothing" - which
784+
# differ under --webp false, where the planner claims the source's own
785+
# name regardless - and so waved genuine drift through as a warning.
786+
if claimed.get(dst_final.name.lower()) != src:
787+
# ValueError, not RuntimeError: that is what insert_optimized_media's
788+
# `except ValueError` around this call turns into a clean
789+
# "error: ..." line, the same as the input_dir == output_dir
790+
# refusal above. `plan.names` rather than `claimed`'s keys, so the
791+
# message shows the claim as the planner wrote it - reporting a
792+
# casefolded "diagram.svg" for a source that claimed "Diagram.SVG"
793+
# points the reader at a case mismatch that isn't the problem.
794+
raise ValueError(
795+
f"{src} wrote {dst_final.name}, which possible_output_names did not predict "
796+
f"(it claimed {sorted(plan.names) or 'nothing'}). The output-name planning pass and "
797+
"process_file have drifted apart - see possible_output_names."
798+
)
768799
written.append(dst_final)
769800
rel_final = dst_final.relative_to(output_dir)
770801
if rel_final != rel:

‎ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/tests/test_review_findings.py‎

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -997,9 +997,15 @@ def test_the_probe_reports_its_own_failure(tmp_path, capsys):
997997

998998
# --- the written-vs-claimed check must not fire on ordinary inputs ----------
999999

1000-
@pytest.mark.parametrize("name", ["Diagram.SVG", "Diagram.Svg", "Photo.PNG", "Photo.JPG", "Notes.TXT"])
1000+
@pytest.mark.parametrize("name, expected_webp, expected_plain", [
1001+
("Diagram.SVG", "Diagram.SVG", "Diagram.SVG"), # small enough not to rasterize
1002+
("Diagram.Svg", "Diagram.Svg", "Diagram.Svg"),
1003+
("Photo.PNG", "Photo.webp", "Photo.PNG"),
1004+
("Photo.JPG", "Photo.webp", "Photo.JPG"),
1005+
("Notes.TXT", "Notes.TXT", "Notes.TXT"), # passthrough copy
1006+
])
10011007
@pytest.mark.parametrize("webp", [True, False])
1002-
def test_an_uppercase_extension_is_not_mistaken_for_drift(tmp_path, name, webp):
1008+
def test_an_uppercase_extension_is_not_mistaken_for_drift(tmp_path, name, expected_webp, expected_plain, webp):
10031009
"""possible_output_names' SVG branch returned the lowercase SVG_EXTENSION
10041010
literal while optimize_svg writes `dst`, which carries the source's own
10051011
spelling - so "Diagram.SVG" was claimed as "Diagram.svg". Harmless while
@@ -1019,7 +1025,10 @@ def test_an_uppercase_extension_is_not_mistaken_for_drift(tmp_path, name, webp):
10191025
pngquant_path=om.find_pngquant(), logger=om.Logger(sys.stdout),
10201026
stats=_new_stats())
10211027

1022-
assert len(result.written) == 1, "the file was optimized, so it must survive the check"
1028+
# The name matters, not just the count: the bug was a claim spelled
1029+
# ".svg" for a file written as ".SVG", and asserting only that one file
1030+
# came out would pass just as happily if the output had been lowercased.
1031+
assert [p.name for p in result.written] == [expected_webp if webp else expected_plain]
10231032

10241033

10251034
def test_the_svg_branch_claims_the_source_s_own_spelling():
@@ -1029,21 +1038,49 @@ def test_the_svg_branch_claims_the_source_s_own_spelling():
10291038
assert om.possible_output_names("d", ".svg", cfg) == {"d.svg", "d.webp"}
10301039

10311040

1032-
def test_an_unpredicted_source_that_does_write_is_kept_not_fatal(tmp_path, monkeypatch, capsys):
1033-
"""A probe that fails where optimize_raster then succeeds is one file's
1034-
bad luck, not drift between the planner and the writer. This module's
1035-
rule is that a single file's problem is reported and the run continues -
1036-
a dangling symlink killing the whole run was fixed as a bug in this same
1037-
PR - so the check must not reintroduce that shape here."""
1041+
@pytest.mark.parametrize("webp", [True, False])
1042+
def test_an_undetermined_probe_skips_one_file_and_no_more(tmp_path, monkeypatch, capsys, webp):
1043+
"""optimize_raster takes the planner's answer rather than re-deriving it,
1044+
so a source whose animation could not be determined is refused there,
1045+
counted as one file's error, and never reaches the written-vs-claimed
1046+
check. The run continues - this module's rule is that a single file's
1047+
problem is reported and the rest of the tree still processes.
1048+
1049+
The --webp False half is the case the old exemption got wrong. The
1050+
planner claims the source's own name there regardless of `animated` (that
1051+
branch returns before consulting it), so "the probe failed" and "the
1052+
planner claimed nothing" came apart - and the exemption, keyed on the
1053+
former, waved genuine drift through as a warning."""
10381054
src, out = tmp_path / "in", tmp_path / "out"
10391055
src.mkdir(), out.mkdir()
10401056
Image.new("RGB", (20, 20), (5, 5, 5)).save(src / "photo.png")
1057+
Image.new("RGB", (20, 20), (6, 6, 6)).save(src / "other.jpg")
10411058
monkeypatch.setattr(om, "_is_animated_raster", lambda source, logger=None: None)
10421059

10431060
stats = _new_stats()
1044-
result = om.optimize_directory(src, out, cfg=dict(om.BUILTIN_DEFAULTS) | {"webp": True},
1061+
result = om.optimize_directory(src, out, cfg=dict(om.BUILTIN_DEFAULTS) | {"webp": webp},
10451062
pngquant_path=om.find_pngquant(), logger=om.Logger(sys.stdout),
10461063
stats=stats)
10471064

1048-
assert [p.name for p in result.written] == ["photo.webp"], "a good file is kept, not discarded"
1049-
assert "the de-confliction pass held no name for it" in capsys.readouterr().out
1065+
assert stats["errors"] == 1, "the undetermined file, and only it"
1066+
assert [p.name for p in result.written] == ["other.webp" if webp else "other.jpg"]
1067+
assert "could not determine whether this file is animated" in capsys.readouterr().out
1068+
1069+
1070+
def test_the_drift_message_names_the_claim_as_the_planner_wrote_it(tmp_path, monkeypatch):
1071+
"""Built from `claimed`'s keys, the message reported a casefolded
1072+
"diagram.svg" for a source that claimed "Diagram.SVG" - pointing whoever
1073+
reads it at a case mismatch that is not the problem."""
1074+
src, out = tmp_path / "in", tmp_path / "out"
1075+
src.mkdir(), out.mkdir()
1076+
(src / "Diagram.SVG").write_text('<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">'
1077+
'<rect width="10" height="10"/></svg>')
1078+
real_process_file = om.process_file
1079+
def drifting_process_file(source, dst, **kwargs):
1080+
written = real_process_file(source, dst, **kwargs)
1081+
return written.with_suffix(".avif") if written else written
1082+
monkeypatch.setattr(om, "process_file", drifting_process_file)
1083+
1084+
with pytest.raises(ValueError, match=r"it claimed \['Diagram\.SVG', 'Diagram\.png'\]"):
1085+
om.optimize_directory(src, out, cfg=dict(om.BUILTIN_DEFAULTS), pngquant_path=om.find_pngquant(),
1086+
logger=om.Logger(sys.stdout), stats=_new_stats())

0 commit comments

Comments
 (0)