From 0b0fae8299c8cff58872f138e2c523096d1c80bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 2 Nov 2026 18:49:54 -0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20fix(cache):=20write=20compac?= =?UTF-8?q?t=20interpreter=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Indented cache writes amplify nested candidate responses, and the streaming encoder can reject input that the JSON parser accepted. Write compact JSON before replacing the cache entry so its size follows the response size. Fixes #204 --- docs/changelog/204.bugfix.rst | 1 + src/python_discovery/_cache.py | 3 ++- tests/test_cache.py | 20 ++++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/204.bugfix.rst diff --git a/docs/changelog/204.bugfix.rst b/docs/changelog/204.bugfix.rst new file mode 100644 index 0000000..a317613 --- /dev/null +++ b/docs/changelog/204.bugfix.rst @@ -0,0 +1 @@ +Write interpreter cache entries as compact JSON to keep their size proportional to the interpreter response. diff --git a/src/python_discovery/_cache.py b/src/python_discovery/_cache.py index 30b8b2e..f4d0c2f 100644 --- a/src/python_discovery/_cache.py +++ b/src/python_discovery/_cache.py @@ -119,7 +119,8 @@ def write(self, content: dict) -> None: mode="w", encoding="utf-8", dir=self._folder, prefix=f".{self._key}.", delete=False ) as file: temporary = Path(file.name) - json.dump(content, file, sort_keys=True, indent=2) + # Compact encoding avoids indentation growth and the streaming encoder's recursion limit. + file.write(json.dumps(content, sort_keys=True)) temporary.replace(self._file) except OSError: _LOGGER.debug("failed to write %s", self._file, exc_info=True) diff --git a/tests/test_cache.py b/tests/test_cache.py index 8e38a41..2223918 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import os import stat import sys @@ -36,6 +37,25 @@ def test_disk_content_store_read_missing_file(tmp_path: Path) -> None: assert store.read() is None +@pytest.mark.parametrize( + "source", + [ + pytest.param(json.dumps("x" * 398), id="flat"), + pytest.param("[" * 200 + "]" * 200, id="nested"), + ], +) +def test_disk_content_store_write_size_tracks_input(tmp_path: Path, source: str) -> None: + content = {"padding": json.loads(source)} + DiskContentStore(tmp_path, "test").write(content) + assert (tmp_path / "test.json").stat().st_size < 2 * len(json.dumps(content)) + + +def test_disk_content_store_write_deep_json(tmp_path: Path) -> None: + depth = 2_000 if sys.version_info >= (3, 15) else 900 + DiskContentStore(tmp_path, "test").write({"padding": json.loads("[" * depth + "]" * depth)}) + assert (tmp_path / "test.json").stat().st_size < 10_000 + + def test_disk_content_store_remove(tmp_path: Path) -> None: store = DiskContentStore(tmp_path, "test") store.write({"key": "value"}) From cee584a6768adf0f01a692990433b4d699565415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 2 Nov 2026 18:58:11 -0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20fix(cache):=20discard=20over?= =?UTF-8?q?sized=20old=20entries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compact writes leave old amplified files in place. Discard entries above the size a bounded query can produce, allowing for Unicode escaping, so discovery re-queries and replaces them. --- src/python_discovery/_cache.py | 4 ++++ tests/test_cache.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/python_discovery/_cache.py b/src/python_discovery/_cache.py index f4d0c2f..1095cac 100644 --- a/src/python_discovery/_cache.py +++ b/src/python_discovery/_cache.py @@ -105,6 +105,10 @@ def _read_entry(self) -> tuple[dict | None, bool]: ): _LOGGER.warning("ignoring untrusted cache entry at %s", self._file) return None, True + # A 1 MiB query can expand to 12 MiB when supplementary Unicode is escaped. + if status.st_size > 16 << 20: + _LOGGER.warning("ignoring oversized cache entry at %s", self._file) + return None, True return json.load(file), False @staticmethod diff --git a/tests/test_cache.py b/tests/test_cache.py index 2223918..8976c28 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -37,6 +37,21 @@ def test_disk_content_store_read_missing_file(tmp_path: Path) -> None: assert store.read() is None +def test_disk_content_store_reads_near_limit_escaped_json(tmp_path: Path) -> None: + content = {"padding": "😀" * ((1 << 20) - 4_096)} + store = DiskContentStore(tmp_path, "test") + store.write(content) + assert store.read() == content + + +def test_disk_content_store_discards_oversized_entry(tmp_path: Path) -> None: + entry = tmp_path / "test.json" + entry.write_text(json.dumps({"padding": "x" * (16 << 20)}), encoding="utf-8") + store = DiskContentStore(tmp_path, "test") + assert store.read() is None + assert not entry.exists() + + @pytest.mark.parametrize( "source", [