From 62dad9fb18e0b136cc8b27305b9cbd1d0da5c2a3 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Tue, 21 Jul 2026 15:51:34 +0200 Subject: [PATCH 1/5] Make FileOutcomeReport labels configurable Add label_overrides so non-add pipelines log the right verb. The image-embedding path now logs embedded=N instead of added=N. raise_if_all_failed() is unaffected; the overrides only touch log_summary(). Existing callers are unchanged (default empty). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../core/file_outcome_report.py | 14 ++++++- .../lightly_studio/dataset/image_embedding.py | 2 +- .../tests/core/test_file_outcome_report.py | 38 +++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/lightly_studio/src/lightly_studio/core/file_outcome_report.py b/lightly_studio/src/lightly_studio/core/file_outcome_report.py index 84512a5458..3f87a75373 100644 --- a/lightly_studio/src/lightly_studio/core/file_outcome_report.py +++ b/lightly_studio/src/lightly_studio/core/file_outcome_report.py @@ -94,9 +94,13 @@ class FileOutcomeReport: Attributes: max_examples_per_outcome: Maximum number of example paths kept per outcome. + label_overrides: Per-outcome display-label overrides used only by + `log_summary()`; any outcome absent from the dict falls back to + `outcome.value`. Defaults to empty (add-images behavior). """ max_examples_per_outcome: int = DEFAULT_MAX_EXAMPLES_PER_OUTCOME + label_overrides: dict[FileOutcome, str] = field(default_factory=dict) _counts: dict[FileOutcome, int] = field( default_factory=lambda: dict.fromkeys(FileOutcome, 0), init=False, @@ -171,11 +175,17 @@ def raise_if_all_failed(self) -> None: f"({n_missing} missing, {n_broken} broken)." ) + def _label(self, outcome: FileOutcome) -> str: + """Return the display label for `outcome`, honoring `label_overrides`.""" + return self.label_overrides.get(outcome, outcome.value) + def log_summary(self) -> None: """Log a single end-of-run summary of counts and example paths.""" - counts = ", ".join(f"{outcome.value}={self._counts[outcome]}" for outcome in FileOutcome) + counts = ", ".join( + f"{self._label(outcome)}={self._counts[outcome]}" for outcome in FileOutcome + ) logger.info(f"File outcomes: {counts}.") for outcome in FileOutcome: examples = self._example_paths[outcome] if examples: - logger.info(f"Example {outcome.value} paths: {', '.join(examples)}.") + logger.info(f"Example {self._label(outcome)} paths: {', '.join(examples)}.") diff --git a/lightly_studio/src/lightly_studio/dataset/image_embedding.py b/lightly_studio/src/lightly_studio/dataset/image_embedding.py index 493005722f..1fc35516cd 100644 --- a/lightly_studio/src/lightly_studio/dataset/image_embedding.py +++ b/lightly_studio/src/lightly_studio/dataset/image_embedding.py @@ -111,7 +111,7 @@ def load_and_preprocess(filepath: str) -> torch.Tensor | None: ) kept_indices_set = set(result.kept_indices) - report = FileOutcomeReport() + report = FileOutcomeReport(label_overrides={FileOutcome.ADDED: "embedded"}) for index, filepath in enumerate(filepaths): outcome = FileOutcome.ADDED if index in kept_indices_set else FileOutcome.BROKEN report.record(path=filepath, outcome=outcome) diff --git a/lightly_studio/tests/core/test_file_outcome_report.py b/lightly_studio/tests/core/test_file_outcome_report.py index b79fb1f826..7230215af0 100644 --- a/lightly_studio/tests/core/test_file_outcome_report.py +++ b/lightly_studio/tests/core/test_file_outcome_report.py @@ -140,3 +140,41 @@ def test_log_summary(self, caplog: pytest.LogCaptureFixture) -> None: assert "missing=1" in text assert "a.jpg" in text assert "b.jpg" in text + assert "Example added paths" in text + + def test_log_summary__label_overrides(self, caplog: pytest.LogCaptureFixture) -> None: + report = FileOutcomeReport(label_overrides={FileOutcome.ADDED: "embedded"}) + report.record(path="a.jpg", outcome=FileOutcome.ADDED) + report.record(path="b.jpg", outcome=FileOutcome.MISSING) + + with caplog.at_level(logging.INFO): + report.log_summary() + + text = caplog.text + # Overridden outcome uses the custom label. + assert "embedded=1" in text + assert "Example embedded paths" in text + assert "added=1" not in text + # Non-overridden outcomes still render their default labels. + assert "missing=1" in text + + def test_log_summary__label_overrides_are_general( + self, caplog: pytest.LogCaptureFixture + ) -> None: + report = FileOutcomeReport( + label_overrides={ + FileOutcome.ADDED: "embedded", + FileOutcome.MISSING: "not_found", + } + ) + report.record(path="a.jpg", outcome=FileOutcome.ADDED) + report.record(path="b.jpg", outcome=FileOutcome.MISSING) + + with caplog.at_level(logging.INFO): + report.log_summary() + + text = caplog.text + assert "embedded=1" in text + assert "not_found=1" in text + assert "Example not_found paths" in text + assert "missing=1" not in text From 0f27785787bb977d2b2d1d8cf04867a96ad4c44b Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Wed, 22 Jul 2026 06:57:47 +0200 Subject: [PATCH 2/5] Address Codex review: Mapping type and helper placement - Type label_overrides as Mapping (read-only) per Python guidelines - Move _label helper below public log_summary Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/lightly_studio/core/file_outcome_report.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lightly_studio/src/lightly_studio/core/file_outcome_report.py b/lightly_studio/src/lightly_studio/core/file_outcome_report.py index 3f87a75373..9996d4e8bc 100644 --- a/lightly_studio/src/lightly_studio/core/file_outcome_report.py +++ b/lightly_studio/src/lightly_studio/core/file_outcome_report.py @@ -37,7 +37,7 @@ import contextlib import logging -from collections.abc import Iterator +from collections.abc import Iterator, Mapping from dataclasses import dataclass, field from enum import Enum from os import PathLike @@ -100,7 +100,7 @@ class FileOutcomeReport: """ max_examples_per_outcome: int = DEFAULT_MAX_EXAMPLES_PER_OUTCOME - label_overrides: dict[FileOutcome, str] = field(default_factory=dict) + label_overrides: Mapping[FileOutcome, str] = field(default_factory=dict) _counts: dict[FileOutcome, int] = field( default_factory=lambda: dict.fromkeys(FileOutcome, 0), init=False, @@ -175,10 +175,6 @@ def raise_if_all_failed(self) -> None: f"({n_missing} missing, {n_broken} broken)." ) - def _label(self, outcome: FileOutcome) -> str: - """Return the display label for `outcome`, honoring `label_overrides`.""" - return self.label_overrides.get(outcome, outcome.value) - def log_summary(self) -> None: """Log a single end-of-run summary of counts and example paths.""" counts = ", ".join( @@ -189,3 +185,7 @@ def log_summary(self) -> None: examples = self._example_paths[outcome] if examples: logger.info(f"Example {self._label(outcome)} paths: {', '.join(examples)}.") + + def _label(self, outcome: FileOutcome) -> str: + """Return the display label for `outcome`, honoring `label_overrides`.""" + return self.label_overrides.get(outcome, outcome.value) From ed00e2c52338ddbcece68db2882abfbab155810c Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Thu, 23 Jul 2026 13:28:13 +0200 Subject: [PATCH 3/5] Simplify example msg --- .../src/lightly_studio/core/file_outcome_report.py | 2 +- .../tests/core/image/test_image_dataset__labelformat.py | 2 +- lightly_studio/tests/core/image/test_image_dataset__path.py | 2 +- lightly_studio/tests/core/test_file_outcome_report.py | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lightly_studio/src/lightly_studio/core/file_outcome_report.py b/lightly_studio/src/lightly_studio/core/file_outcome_report.py index 9996d4e8bc..c2b8500465 100644 --- a/lightly_studio/src/lightly_studio/core/file_outcome_report.py +++ b/lightly_studio/src/lightly_studio/core/file_outcome_report.py @@ -184,7 +184,7 @@ def log_summary(self) -> None: for outcome in FileOutcome: examples = self._example_paths[outcome] if examples: - logger.info(f"Example {self._label(outcome)} paths: {', '.join(examples)}.") + logger.info(f"Examples '{self._label(outcome)}': {', '.join(examples)}.") def _label(self, outcome: FileOutcome) -> str: """Return the display label for `outcome`, honoring `label_overrides`.""" diff --git a/lightly_studio/tests/core/image/test_image_dataset__labelformat.py b/lightly_studio/tests/core/image/test_image_dataset__labelformat.py index 992c885b54..770de00887 100644 --- a/lightly_studio/tests/core/image/test_image_dataset__labelformat.py +++ b/lightly_studio/tests/core/image/test_image_dataset__labelformat.py @@ -135,7 +135,7 @@ def test_from_labelformat__duplication( log_text = caplog.text assert "added=0, already_present=1" in log_text - assert "Example already_present paths:" in log_text + assert "Examples 'already_present':" in log_text assert "/fake/path/images/image.jpg" in log_text def test_from_labelformat__annotations_synced_images( diff --git a/lightly_studio/tests/core/image/test_image_dataset__path.py b/lightly_studio/tests/core/image/test_image_dataset__path.py index 2c8bec675a..2ce2480fdf 100644 --- a/lightly_studio/tests/core/image/test_image_dataset__path.py +++ b/lightly_studio/tests/core/image/test_image_dataset__path.py @@ -180,7 +180,7 @@ def test_dataset_add_images_from_path__duplication( log_text = caplog.text assert "added=2, already_present=4" in log_text - assert "Example already_present paths:" in log_text + assert "Examples 'already_present':" in log_text assert f"{images_path}" in log_text def test_dataset_add_images_from_path__dont_embed( diff --git a/lightly_studio/tests/core/test_file_outcome_report.py b/lightly_studio/tests/core/test_file_outcome_report.py index 7230215af0..14b3eab7c7 100644 --- a/lightly_studio/tests/core/test_file_outcome_report.py +++ b/lightly_studio/tests/core/test_file_outcome_report.py @@ -140,7 +140,7 @@ def test_log_summary(self, caplog: pytest.LogCaptureFixture) -> None: assert "missing=1" in text assert "a.jpg" in text assert "b.jpg" in text - assert "Example added paths" in text + assert "Examples 'added'" in text def test_log_summary__label_overrides(self, caplog: pytest.LogCaptureFixture) -> None: report = FileOutcomeReport(label_overrides={FileOutcome.ADDED: "embedded"}) @@ -153,7 +153,7 @@ def test_log_summary__label_overrides(self, caplog: pytest.LogCaptureFixture) -> text = caplog.text # Overridden outcome uses the custom label. assert "embedded=1" in text - assert "Example embedded paths" in text + assert "Examples 'embedded'" in text assert "added=1" not in text # Non-overridden outcomes still render their default labels. assert "missing=1" in text @@ -176,5 +176,5 @@ def test_log_summary__label_overrides_are_general( text = caplog.text assert "embedded=1" in text assert "not_found=1" in text - assert "Example not_found paths" in text + assert "Examples 'not_found'" in text assert "missing=1" not in text From eb1fe7d95fecfa1fc6e7326093c68724d522f1b4 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Thu, 23 Jul 2026 13:36:37 +0200 Subject: [PATCH 4/5] Update prefix --- lightly_studio/src/lightly_studio/core/file_outcome_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightly_studio/src/lightly_studio/core/file_outcome_report.py b/lightly_studio/src/lightly_studio/core/file_outcome_report.py index c2b8500465..1fcb9b616d 100644 --- a/lightly_studio/src/lightly_studio/core/file_outcome_report.py +++ b/lightly_studio/src/lightly_studio/core/file_outcome_report.py @@ -180,7 +180,7 @@ def log_summary(self) -> None: counts = ", ".join( f"{self._label(outcome)}={self._counts[outcome]}" for outcome in FileOutcome ) - logger.info(f"File outcomes: {counts}.") + logger.info(f"File processing result: {counts}.") for outcome in FileOutcome: examples = self._example_paths[outcome] if examples: From 21656d9cc00f9b76891f43289b95c585b5246d65 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Thu, 23 Jul 2026 13:40:12 +0200 Subject: [PATCH 5/5] Do not show examples for success --- .../src/lightly_studio/core/file_outcome_report.py | 2 ++ lightly_studio/tests/core/test_file_outcome_report.py | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lightly_studio/src/lightly_studio/core/file_outcome_report.py b/lightly_studio/src/lightly_studio/core/file_outcome_report.py index 1fcb9b616d..89e46369e9 100644 --- a/lightly_studio/src/lightly_studio/core/file_outcome_report.py +++ b/lightly_studio/src/lightly_studio/core/file_outcome_report.py @@ -182,6 +182,8 @@ def log_summary(self) -> None: ) logger.info(f"File processing result: {counts}.") for outcome in FileOutcome: + if outcome == FileOutcome.ADDED: + continue examples = self._example_paths[outcome] if examples: logger.info(f"Examples '{self._label(outcome)}': {', '.join(examples)}.") diff --git a/lightly_studio/tests/core/test_file_outcome_report.py b/lightly_studio/tests/core/test_file_outcome_report.py index 14b3eab7c7..7a08106c7a 100644 --- a/lightly_studio/tests/core/test_file_outcome_report.py +++ b/lightly_studio/tests/core/test_file_outcome_report.py @@ -138,9 +138,11 @@ def test_log_summary(self, caplog: pytest.LogCaptureFixture) -> None: text = caplog.text assert "added=1" in text assert "missing=1" in text - assert "a.jpg" in text + # Examples are only shown for non-ADDED outcomes. assert "b.jpg" in text - assert "Examples 'added'" in text + assert "Examples 'missing'" in text + assert "a.jpg" not in text + assert "Examples 'added'" not in text def test_log_summary__label_overrides(self, caplog: pytest.LogCaptureFixture) -> None: report = FileOutcomeReport(label_overrides={FileOutcome.ADDED: "embedded"}) @@ -153,7 +155,6 @@ def test_log_summary__label_overrides(self, caplog: pytest.LogCaptureFixture) -> text = caplog.text # Overridden outcome uses the custom label. assert "embedded=1" in text - assert "Examples 'embedded'" in text assert "added=1" not in text # Non-overridden outcomes still render their default labels. assert "missing=1" in text