From 192fbede5fc4b407fe1dad12c5f9ddb8a1e6608d Mon Sep 17 00:00:00 2001 From: Dmitrii Safronov Date: Mon, 26 Oct 2026 01:05:33 +0400 Subject: [PATCH 1/2] fix: guard validation diagnostics against broken repr Signed-off-by: Dmitrii Safronov --- .../schema_applier.py | 21 +++++-- tests/private/test_schema_logger.py | 62 +++++++++++++++++++ 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/logging_objects_with_schema/schema_applier.py b/src/logging_objects_with_schema/schema_applier.py index 487dbd2..29e0838 100644 --- a/src/logging_objects_with_schema/schema_applier.py +++ b/src/logging_objects_with_schema/schema_applier.py @@ -11,11 +11,20 @@ from .schema_loader import _CompiledSchema, _SchemaLeaf +def _safe_repr(value: Any) -> str: + """Represent diagnostic data without propagating user __repr__ failures.""" + try: + return repr(value) + except Exception: + # Do not format the value or exception again: either may have a broken repr. + return "" + + def _create_validation_error_dict(field: str, error: str, value: Any) -> dict[str, Any]: """Create a dict for a single validation error. - All values are wrapped in repr() to ensure any type is representable - and special characters cannot cause issues downstream. + All values use safe repr() formatting. If user code raises during repr(), + a constant placeholder preserves the validation diagnostic. Args: field: Field name that caused the validation error. @@ -23,12 +32,12 @@ def _create_validation_error_dict(field: str, error: str, value: Any) -> dict[st value: Invalid value that caused the error. Returns: - Dict with field, error, and value (all via repr() for safety). + Dict with field, error, and value as safely represented strings. """ return { - "field": repr(field), - "error": repr(error), - "value": repr(value), + "field": _safe_repr(field), + "error": _safe_repr(error), + "value": _safe_repr(value), } diff --git a/tests/private/test_schema_logger.py b/tests/private/test_schema_logger.py index d6ad39e..d0ccc2d 100644 --- a/tests/private/test_schema_logger.py +++ b/tests/private/test_schema_logger.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json import logging from io import StringIO from pathlib import Path @@ -594,3 +595,64 @@ def emit(self, record): if len(messages) > 1: assert messages[1][0] == logging.ERROR assert "validation_errors" in messages[1][1] + + +@pytest.mark.parametrize("failure", ["raises", "non_string"]) +@pytest.mark.parametrize("case", ["wrong_type", "unknown", "empty_schema", "list"]) +def test_invalid_extra_with_broken_repr( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + failure: str, + case: str, +) -> None: + """Broken user repr must not prevent emission or aggregated diagnostics.""" + + class BrokenRepr: + def __repr__(self): + if failure == "raises": + raise RuntimeError("repr failed") + return 42 + + monkeypatch.chdir(tmp_path) + schema = { + "ServicePayload": { + "RequestID": {"type": "str", "source": "request_id"}, + "UserID": {"type": "int", "source": "user_id"}, + "Tags": {"type": "list", "source": "tags", "item_type": "str"}, + } + } + _write_schema(tmp_path, {} if case == "empty_schema" else schema) + value = BrokenRepr() + field = { + "wrong_type": "user_id", + "unknown": "unknown", + "empty_schema": "unknown", + "list": "tags", + }[case] + extra = {field: [value] if case == "list" else value} + if case != "empty_schema": + extra.update( + request_id="abc-123", user_id=None if case != "wrong_type" else value + ) + + logger = SchemaLogger("broken_repr", logging.INFO) + records = [] + + class CaptureHandler(logging.Handler): + def emit(self, record): + records.append(record) + + logger.addHandler(CaptureHandler()) + logger.info("main message", extra=extra) + + assert [record.levelno for record in records] == [logging.INFO, logging.ERROR] + assert records[0].getMessage() == "main message" + if case == "empty_schema": + assert not hasattr(records[0], "ServicePayload") + else: + assert records[0].ServicePayload == {"RequestID": "abc-123"} + errors = json.loads(records[1].getMessage())["validation_errors"] + problem = next(error for error in errors if error["field"] == repr(field)) + assert problem["value"] == "" + assert all(isinstance(value, str) for error in errors for value in error.values()) + assert len(errors) == (2 if case in {"unknown", "list"} else 1) From b53bda638eb0443027bc19ec62e14d018d089b61 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 25 Oct 2026 21:08:29 +0000 Subject: [PATCH 2/2] chore(release): 1.0.3-rc.1 ## [1.0.3-rc.1](https://github.com/disafronov/python-logging-objects-with-schema/compare/v1.0.2...v1.0.3-rc.1) (2026-10-25) ### Bug Fixes * guard validation diagnostics against broken repr ([192fbed](https://github.com/disafronov/python-logging-objects-with-schema/commit/192fbede5fc4b407fe1dad12c5f9ddb8a1e6608d)) Signed-off-by: semantic-release-bot --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7590dca..996d9df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.0.3-rc.1](https://github.com/disafronov/python-logging-objects-with-schema/compare/v1.0.2...v1.0.3-rc.1) (2026-10-25) + +### Bug Fixes + +* guard validation diagnostics against broken repr ([192fbed](https://github.com/disafronov/python-logging-objects-with-schema/commit/192fbede5fc4b407fe1dad12c5f9ddb8a1e6608d)) + ## [1.0.2](https://github.com/disafronov/python-logging-objects-with-schema/compare/v1.0.1...v1.0.2) (2026-10-25) ### Bug Fixes diff --git a/pyproject.toml b/pyproject.toml index f52dde4..02df7b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "uv_build" [project] name = "logging-objects-with-schema" -version = "1.0.2" +version = "1.0.3rc1" description = "Schema-aware Python logging with strict extra validation and schema-driven field projection." readme = "README.md" requires-python = ">=3.11" diff --git a/uv.lock b/uv.lock index fbbbffb..8011222 100644 --- a/uv.lock +++ b/uv.lock @@ -2313,7 +2313,7 @@ version = "0.16.0" [[package]] name = "logging-objects-with-schema" -version = "1.0.2" +version = "1.0.3rc1" [package.source] editable = "."