From 8591da4a978996cc08e1415daa7507b23ff4ad5c Mon Sep 17 00:00:00 2001 From: Bartok9 Date: Wed, 22 Jul 2026 02:11:26 -0400 Subject: [PATCH 1/2] fix(pydantic): skip non-dict models in list_models Corrupted MDL rows caused KeyError/TypeError in wren_list_models. Skip non-dicts and entries without name; coerce columns/properties. --- sdk/wren-pydantic/src/wren_pydantic/_tools.py | 29 ++++-- .../tests/unit/test_run_list_models_guard.py | 88 +++++++++++++++++++ 2 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 sdk/wren-pydantic/tests/unit/test_run_list_models_guard.py diff --git a/sdk/wren-pydantic/src/wren_pydantic/_tools.py b/sdk/wren-pydantic/src/wren_pydantic/_tools.py index dd49861bda..2599814a8d 100644 --- a/sdk/wren-pydantic/src/wren_pydantic/_tools.py +++ b/sdk/wren-pydantic/src/wren_pydantic/_tools.py @@ -169,11 +169,26 @@ def _run_list_models(toolkit: WrenToolkit) -> list[ModelSummary]: raise to_model_retry(exc) from exc models = manifest.get("models") or [] - return [ - ModelSummary( - name=m["name"], - column_count=len(m.get("columns") or []), - description=(m.get("properties") or {}).get("description"), + # Skip non-dict rows and tolerate missing name/columns/properties so a + # single corrupted model entry cannot KeyError/TypeError the tool. + summaries: list[ModelSummary] = [] + for m in models: + if not isinstance(m, dict): + continue + name = m.get("name") + if not name: + continue + columns = m.get("columns") or [] + if not isinstance(columns, list): + columns = [] + props = m.get("properties") or {} + if not isinstance(props, dict): + props = {} + summaries.append( + ModelSummary( + name=name, + column_count=len(columns), + description=props.get("description"), + ) ) - for m in models - ] + return summaries diff --git a/sdk/wren-pydantic/tests/unit/test_run_list_models_guard.py b/sdk/wren-pydantic/tests/unit/test_run_list_models_guard.py new file mode 100644 index 0000000000..abfab43ab3 --- /dev/null +++ b/sdk/wren-pydantic/tests/unit/test_run_list_models_guard.py @@ -0,0 +1,88 @@ +"""_run_list_models skips non-dict / incomplete model rows.""" +import ast +import importlib.util +import sys +import types +from pathlib import Path +from unittest.mock import MagicMock + +ROOT = Path(__file__).resolve().parents[2] +SRC = ROOT / "src" + +# Stub heavy deps before loading _tools +sys.path.insert(0, str(SRC)) + +# Provide minimal ModelSummary used by _tools +models_mod = types.ModuleType("wren_pydantic._models") + + +class ModelSummary: + def __init__(self, name, column_count, description=None): + self.name = name + self.column_count = column_count + self.description = description + + +models_mod.ModelSummary = ModelSummary +sys.modules["wren_pydantic._models"] = models_mod + +# Other imports in _tools — stub at package boundary by executing only the function +# Extract function source with ast and exec in isolation is heavy; load module with stubs. + +for name in [ + "wren", + "wren.model", + "wren.model.error", + "wren.engine", + "pydantic_ai", + "pydantic_ai.exceptions", + "wren_pydantic", + "wren_pydantic._errors", + "wren_pydantic._toolkit", +]: + sys.modules.setdefault(name, types.ModuleType(name)) + +sys.modules["wren.model.error"].WrenError = type("WrenError", (Exception,), {}) +sys.modules["wren_pydantic._errors"].should_propagate = lambda e: False +sys.modules["wren_pydantic._errors"].to_model_retry = lambda e: e + +# _tools imports many symbols — read file and exec just _run_list_models after injecting names +src = (SRC / "wren_pydantic" / "_tools.py").read_text() +# Pull the function body by line markers +start = src.index("def _run_list_models") +# until next top-level def at same indent +rest = src[start:] +lines = rest.splitlines(True) +body = [lines[0]] +for line in lines[1:]: + if line.startswith("def ") or line.startswith("async def ") or line.startswith("class "): + break + body.append(line) +code = "".join(body) +ns = { + "ModelSummary": ModelSummary, + "WrenError": sys.modules["wren.model.error"].WrenError, + "should_propagate": sys.modules["wren_pydantic._errors"].should_propagate, + "to_model_retry": sys.modules["wren_pydantic._errors"].to_model_retry, + "list": list, +} +exec(code, ns) +_run_list_models = ns["_run_list_models"] + + +def test_skips_bad_models(): + toolkit = MagicMock() + toolkit._mdl_source.load_manifest.return_value = { + "models": [ + None, + "x", + {"columns": []}, # no name + {"name": "ok", "columns": None, "properties": "nope"}, + {"name": "t", "columns": [{"n": 1}], "properties": {"description": "d"}}, + ] + } + out = _run_list_models(toolkit) + assert [m.name for m in out] == ["ok", "t"] + assert out[0].column_count == 0 + assert out[1].column_count == 1 + assert out[1].description == "d" From a929a0be878806fd355a389feaf7bbe80094948e Mon Sep 17 00:00:00 2001 From: Bartok9 Date: Wed, 22 Jul 2026 04:09:18 -0400 Subject: [PATCH 2/2] test(pydantic): fix list_models guard without poisoning _models imports The previous own loads stubbed wren_pydantic._models and exec'd a slice of _tools, which caused collection NameError/ImportError for sibling unit tests. Use the real _run_list_models entrypoint with a MagicMock toolkit instead. --- .../tests/unit/test_run_list_models_guard.py | 69 +------------------ 1 file changed, 3 insertions(+), 66 deletions(-) diff --git a/sdk/wren-pydantic/tests/unit/test_run_list_models_guard.py b/sdk/wren-pydantic/tests/unit/test_run_list_models_guard.py index abfab43ab3..526714a7a7 100644 --- a/sdk/wren-pydantic/tests/unit/test_run_list_models_guard.py +++ b/sdk/wren-pydantic/tests/unit/test_run_list_models_guard.py @@ -1,73 +1,10 @@ """_run_list_models skips non-dict / incomplete model rows.""" -import ast -import importlib.util -import sys -import types -from pathlib import Path -from unittest.mock import MagicMock - -ROOT = Path(__file__).resolve().parents[2] -SRC = ROOT / "src" - -# Stub heavy deps before loading _tools -sys.path.insert(0, str(SRC)) - -# Provide minimal ModelSummary used by _tools -models_mod = types.ModuleType("wren_pydantic._models") - -class ModelSummary: - def __init__(self, name, column_count, description=None): - self.name = name - self.column_count = column_count - self.description = description +from __future__ import annotations +from unittest.mock import MagicMock -models_mod.ModelSummary = ModelSummary -sys.modules["wren_pydantic._models"] = models_mod - -# Other imports in _tools — stub at package boundary by executing only the function -# Extract function source with ast and exec in isolation is heavy; load module with stubs. - -for name in [ - "wren", - "wren.model", - "wren.model.error", - "wren.engine", - "pydantic_ai", - "pydantic_ai.exceptions", - "wren_pydantic", - "wren_pydantic._errors", - "wren_pydantic._toolkit", -]: - sys.modules.setdefault(name, types.ModuleType(name)) - -sys.modules["wren.model.error"].WrenError = type("WrenError", (Exception,), {}) -sys.modules["wren_pydantic._errors"].should_propagate = lambda e: False -sys.modules["wren_pydantic._errors"].to_model_retry = lambda e: e - -# _tools imports many symbols — read file and exec just _run_list_models after injecting names -src = (SRC / "wren_pydantic" / "_tools.py").read_text() -# Pull the function body by line markers -start = src.index("def _run_list_models") -# until next top-level def at same indent -rest = src[start:] -lines = rest.splitlines(True) -body = [lines[0]] -for line in lines[1:]: - if line.startswith("def ") or line.startswith("async def ") or line.startswith("class "): - break - body.append(line) -code = "".join(body) -ns = { - "ModelSummary": ModelSummary, - "WrenError": sys.modules["wren.model.error"].WrenError, - "should_propagate": sys.modules["wren_pydantic._errors"].should_propagate, - "to_model_retry": sys.modules["wren_pydantic._errors"].to_model_retry, - "list": list, -} -exec(code, ns) -_run_list_models = ns["_run_list_models"] +from wren_pydantic._tools import _run_list_models def test_skips_bad_models():