From a6a4849005a2fe179bf152141adbf2767893e567 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 17:11:10 +0000 Subject: [PATCH 1/2] Quiet the notebook output: no debug flood from ImageJ TIFFs is_projected_copy parsed every TIFF description as OME-XML, so each ImageJ image logged a debug ParseError traceback. Only an OME-XML description is parsed now; a malformed one logs one debug line without a traceback. The notebook's log capture set the package logger to DEBUG while records still propagated to the root logger (Colab prints them). While attached, propagation is off and WARNING and above are forwarded to the root handlers. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc --- CHANGELOG.md | 9 +++++ src/morphobricks_preprocessing/notebook_ui.py | 19 +++++++++++ .../projection_marker.py | 10 ++++-- tests/test_notebook_log_capture.py | 33 +++++++++++++++++++ tests/test_projection_marker.py | 31 +++++++++++++++++ 5 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 tests/test_notebook_log_capture.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fceffc7..2f9ff61a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ v0.5.1: no run before v0.5.1 is read.) ## Unreleased (v0.5.1) +### Fixed: the notebook output is no longer flooded with debug messages + +ImageJ TIFFs have a plain-text description, so reading it as OME-XML failed for every image and +logged a debug message with a traceback; and the notebook's own log capture set the package +logger to DEBUG while records still reached the root logger, which Colab prints. Now only an +OME-XML description is parsed (which images are marked is unchanged; a malformed one logs one +debug line without a traceback), and while the capture is attached the package's DEBUG records +go to the "Technical details" panel only, with WARNING and above still reaching the root handlers. + ### The notebook has three code cells: install, mount Google Drive, run Owner request of 10 November 2026, as in the lab's ZeroCostDL4Mic and NucleiSky notebooks. The diff --git a/src/morphobricks_preprocessing/notebook_ui.py b/src/morphobricks_preprocessing/notebook_ui.py index 26c76285..79aa3775 100644 --- a/src/morphobricks_preprocessing/notebook_ui.py +++ b/src/morphobricks_preprocessing/notebook_ui.py @@ -488,6 +488,18 @@ def attached(self): # noqa: ANN202 return _Attached(self) +class _ToRootWarnings(logging.Handler): + """Hands WARNING and above to the root logger's handlers (as propagation would).""" + + def __init__(self) -> None: + super().__init__(logging.WARNING) + + def emit(self, record: logging.LogRecord) -> None: + for handler in logging.getLogger().handlers: + if record.levelno >= handler.level: + handler.handle(record) + + class _Attached: def __init__(self, handler: logging.Handler): # noqa: ANN204 self.handler = handler @@ -495,11 +507,18 @@ def __init__(self, handler: logging.Handler): # noqa: ANN204 def __enter__(self): # noqa: ANN204 self.level = self.logger.level + self.propagate = self.logger.propagate self.logger.addHandler(self.handler) self.logger.setLevel(logging.DEBUG) + # DEBUG is for the panel only: a root handler (Colab's) would print every record. + self.logger.propagate = False + self.forward = _ToRootWarnings() + self.logger.addHandler(self.forward) return self.handler def __exit__(self, *exc): # noqa: ANN002, ANN204 self.logger.removeHandler(self.handler) + self.logger.removeHandler(self.forward) + self.logger.propagate = self.propagate self.logger.setLevel(self.level) return False diff --git a/src/morphobricks_preprocessing/projection_marker.py b/src/morphobricks_preprocessing/projection_marker.py index aa34b065..6c07cea9 100644 --- a/src/morphobricks_preprocessing/projection_marker.py +++ b/src/morphobricks_preprocessing/projection_marker.py @@ -35,10 +35,16 @@ def is_projected_copy(path: Path) -> bool: try: with tifffile.TiffFile(path) as tif: text = tif.pages.first.description - root = ElementTree.fromstring(text) - except (OSError, ValueError, ElementTree.ParseError, tifffile.TiffFileError, AttributeError): + except (OSError, ValueError, tifffile.TiffFileError, AttributeError): logger.debug("Cannot read the description of %s", path, exc_info=True) return False + if not text.lstrip().startswith("<"): + return False # plain text (ImageJ, none): not OME-XML, so not marked; nothing to report + try: + root = ElementTree.fromstring(text) + except ElementTree.ParseError as exc: + logger.debug("The OME description of %s is not well-formed XML (%s)", path, exc) + return False for image in root: if image.tag.rsplit("}", 1)[-1] != "Image": continue diff --git a/tests/test_notebook_log_capture.py b/tests/test_notebook_log_capture.py new file mode 100644 index 00000000..24d52bca --- /dev/null +++ b/tests/test_notebook_log_capture.py @@ -0,0 +1,33 @@ +"""The notebook's log capture keeps DEBUG off the root handlers (Colab prints them).""" + +import io +import logging + +import pytest + +pytest.importorskip("ipywidgets") +pytest.importorskip("matplotlib") + +from morphobricks_preprocessing import notebook_ui # noqa: E402 + + +def test_attached_log_capture_keeps_debug_off_the_root_handlers_but_not_warnings(): + root_stream = io.StringIO() + root_handler = logging.StreamHandler(root_stream) + root_handler.setLevel(logging.NOTSET) + root = logging.getLogger() + root.addHandler(root_handler) + package = logging.getLogger("morphobricks_preprocessing") + before = (package.level, package.propagate, list(package.handlers)) + try: + capture = notebook_ui._LogCapture() + with capture.attached(): + child = logging.getLogger("morphobricks_preprocessing.projection_marker") + child.debug("quiet detail") + child.warning("loud warning") + assert "quiet detail" not in root_stream.getvalue() + assert "loud warning" in root_stream.getvalue() + assert any("quiet detail" in line for line in capture.lines) + assert (package.level, package.propagate, list(package.handlers)) == before + finally: + root.removeHandler(root_handler) diff --git a/tests/test_projection_marker.py b/tests/test_projection_marker.py index cc60bf08..fa6450dc 100644 --- a/tests/test_projection_marker.py +++ b/tests/test_projection_marker.py @@ -80,3 +80,34 @@ def no_pixels(self, *args, **kwargs): from morphobricks_preprocessing.projection_marker import marked_images assert marked_images(copies, NAMES) == sorted(NAMES) + + +def _record_marker_logs(caplog): + caplog.set_level("DEBUG", logger="morphobricks_preprocessing") + + +def test_an_imagej_tiff_is_not_marked_and_logs_nothing(tmp_path, caplog): + from morphobricks_preprocessing.projection_marker import is_projected_copy + + path = tmp_path / "a.tif" + tifffile.imwrite(path, np.zeros((2, 4, 4), np.uint8), imagej=True) + _record_marker_logs(caplog) + assert is_projected_copy(path) is False + assert caplog.records == [] + + +def test_a_malformed_ome_description_is_not_marked_and_logs_no_traceback(tmp_path, caplog): + from morphobricks_preprocessing.projection_marker import is_projected_copy + + path = tmp_path / "b.tif" + tifffile.imwrite(path, np.zeros((4, 4), np.uint8), description="") + _record_marker_logs(caplog) + assert is_projected_copy(path) is False + assert len(caplog.records) == 1 and not caplog.records[0].exc_info + + +def test_a_marked_projection_is_still_marked(tmp_path): + from morphobricks_preprocessing.projection_marker import marked_images + + copies = projected_tree(tmp_path) + assert set(marked_images(copies, NAMES)) == set(NAMES) From 47cbb4a3821a559e7a545848e52582a5909ba57e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 17:50:53 +0000 Subject: [PATCH 2/2] Notebook log capture: forward warnings to logging.lastResort when the root has no handler Without a root handler (plain Jupyter), propagation would have reached logging.lastResort; the forwarder now does the same, so package warnings stay visible. Regression test fails without the fix. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc --- src/morphobricks_preprocessing/notebook_ui.py | 6 ++++-- tests/test_notebook_log_capture.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/morphobricks_preprocessing/notebook_ui.py b/src/morphobricks_preprocessing/notebook_ui.py index 79aa3775..ad250ecd 100644 --- a/src/morphobricks_preprocessing/notebook_ui.py +++ b/src/morphobricks_preprocessing/notebook_ui.py @@ -495,8 +495,10 @@ def __init__(self) -> None: super().__init__(logging.WARNING) def emit(self, record: logging.LogRecord) -> None: - for handler in logging.getLogger().handlers: - if record.levelno >= handler.level: + # With no root handler (plain Jupyter), propagation would reach logging.lastResort. + handlers = logging.getLogger().handlers or [logging.lastResort] + for handler in handlers: + if handler is not None and record.levelno >= handler.level: handler.handle(record) diff --git a/tests/test_notebook_log_capture.py b/tests/test_notebook_log_capture.py index 24d52bca..5a500bba 100644 --- a/tests/test_notebook_log_capture.py +++ b/tests/test_notebook_log_capture.py @@ -31,3 +31,17 @@ def test_attached_log_capture_keeps_debug_off_the_root_handlers_but_not_warnings assert (package.level, package.propagate, list(package.handlers)) == before finally: root.removeHandler(root_handler) + + +def test_a_warning_still_reaches_the_last_resort_handler_without_root_handlers(monkeypatch): + stream = io.StringIO() + monkeypatch.setattr(logging, "lastResort", logging.StreamHandler(stream)) + root = logging.getLogger() + monkeypatch.setattr(root, "handlers", []) + capture = notebook_ui._LogCapture() + with capture.attached(): + child = logging.getLogger("morphobricks_preprocessing.projection_marker") + child.debug("quiet detail") + child.warning("loud warning") + assert "loud warning" in stream.getvalue() + assert "quiet detail" not in stream.getvalue()