From fbd029e3e208a9b32b680e76b3ecc2a5635d12d2 Mon Sep 17 00:00:00 2001 From: ChrisW09 <50968720+ChrisW09@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:52:06 +0200 Subject: [PATCH] fix(core): encode paths and other non-JSON values in lifecycle.jsonl The JSONL event encoder handled only NumPy integers and floats and then raised for any other value. With log_to_file=True and verbosity=3, save(pathlib.Path(...)) therefore failed on its save_started event before writing the artifact. Encode paths with os.fspath, NumPy booleans and arrays as JSON values, and anything else by its string form, as write_run_config() and write_run_summary() already do. Fixes #545 --- deeptab/core/observability.py | 11 +++++++- tests/core/test_observability.py | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/deeptab/core/observability.py b/deeptab/core/observability.py index bfb698eb..7fb963ce 100644 --- a/deeptab/core/observability.py +++ b/deeptab/core/observability.py @@ -370,9 +370,18 @@ def default(self, o: Any) -> Any: return int(o) if isinstance(o, _np.floating): return float(o) + if isinstance(o, _np.bool_): + return bool(o) + if isinstance(o, _np.ndarray): + return o.tolist() except ImportError: pass - return super().default(o) + if isinstance(o, os.PathLike): + return os.fspath(o) + # Logging an event must never make the operation that emitted it + # fail, so any other value is recorded by its string form, as in + # write_run_summary() and write_run_config(). + return str(o) # ----------------------------------------------------------------------- # File handle — opened once per run, line-buffered diff --git a/tests/core/test_observability.py b/tests/core/test_observability.py index 3f02817b..a1e1a77d 100644 --- a/tests/core/test_observability.py +++ b/tests/core/test_observability.py @@ -202,3 +202,50 @@ def test_build_lightning_loggers_user_logger_does_not_replace(monkeypatch): result = build_lightning_loggers(cfg) assert len(result) == 2 assert result[-1] is user_logger + + +def test_lifecycle_jsonl_records_values_json_cannot_encode(monkeypatch, tmp_path): + """Paths, NumPy values and other objects must not make event logging fail.""" + import json + + import numpy as np + + monkeypatch.setitem(sys.modules, "structlog", MagicMock()) + logger = build_structlog_logger( + ObservabilityConfig(structured_logging=True, log_to_console=False, log_to_file=True, verbosity=3), + run_dir=str(tmp_path), + ) + logger.info( + "save_started", + path=tmp_path / "model.deeptab", + count=np.int64(3), + flag=np.bool_(True), + values=np.arange(2), + other=ModuleType("custom"), + ) + record = json.loads((tmp_path / "lifecycle.jsonl").read_text(encoding="utf-8").splitlines()[-1]) + assert record["path"] == str(tmp_path / "model.deeptab") + assert record["count"] == 3 + assert record["flag"] is True + assert record["values"] == [0, 1] + assert record["other"] == str(ModuleType("custom")) + + +def test_save_to_a_path_object_with_file_logging(tmp_path): + pytest.importorskip("structlog") + import numpy as np + + from deeptab.models import MLPRegressor + + observability = ObservabilityConfig( + root_dir=str(tmp_path), structured_logging=True, log_to_console=False, log_to_file=True, verbosity=3 + ) + features = np.random.default_rng(0).normal(size=(60, 3)) + model = MLPRegressor(observability_config=observability, random_state=0) + model.fit(features, features[:, 0], max_epochs=1, accelerator="cpu", enable_progress_bar=False, logger=False) + + target = tmp_path / "model.deeptab" + model.save(target) + assert target.is_file() + (lifecycle,) = (tmp_path / "runs").rglob("lifecycle.jsonl") + assert "save_completed" in lifecycle.read_text(encoding="utf-8")