Skip to content

Commit 788c642

Browse files
fix: resolve mutable defaults, narrow exception handling, and sys.modules leak (#33)
1 parent ebab441 commit 788c642

6 files changed

Lines changed: 57 additions & 24 deletions

File tree

src/evalwire/evaluators/contains.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,17 @@
88
def make_contains_evaluator() -> Callable[[str, dict], bool]:
99
"""Return a substring-containment evaluator.
1010
11-
Checks whether the first value in ``expected["expected_output"]`` appears
12-
as a substring of ``output``. Useful for free-text generation tasks where
13-
the answer must include a specific phrase or keyword.
11+
Checks whether the **first** value in ``expected["expected_output"]``
12+
appears as a substring of ``output``. Useful for free-text generation
13+
tasks where the answer must include a specific phrase or keyword.
14+
15+
.. note::
16+
17+
Only the first item of ``expected_output`` is checked. If the
18+
expected value contains pipe-delimited alternatives (e.g.
19+
``"phrase1|phrase2"``), only ``"phrase1"`` is used after splitting.
20+
Use the ``membership`` evaluator if you need to check whether the
21+
output matches any one of several accepted values.
1422
1523
To test the reverse (output is a substring of the expected string), wrap
1624
the result with ``not``::

src/evalwire/runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ def _load_attribute(self, path: Path, attribute: str) -> Any:
247247
spec.loader.exec_module(module) # type: ignore[union-attr]
248248
return getattr(module, attribute, None)
249249
except Exception as exc:
250+
sys.modules.pop(module_name, None)
250251
logger.error("Failed to load %s: %s", path, exc, exc_info=True)
251252
return None
252253

src/evalwire/uploader.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,17 @@ def __init__(
4141
self,
4242
csv_path: Path | str,
4343
phoenix_client: Client,
44-
input_keys: list[str] = ["user_query"], # noqa: B006
45-
output_keys: list[str] = ["expected_output"], # noqa: B006
44+
input_keys: list[str] | None = None,
45+
output_keys: list[str] | None = None,
4646
tag_column: str = "tags",
4747
delimiter: str = "|",
4848
) -> None:
4949
self.csv_path = Path(csv_path)
5050
self.client = phoenix_client
51-
self.input_keys = list(input_keys)
52-
self.output_keys = list(output_keys)
51+
self.input_keys = list(input_keys) if input_keys is not None else ["user_query"]
52+
self.output_keys = (
53+
list(output_keys) if output_keys is not None else ["expected_output"]
54+
)
5355
self.tag_column = tag_column
5456
self.delimiter = delimiter
5557

@@ -130,12 +132,10 @@ def _upload_one(
130132
existing = self.client.datasets.get_dataset(dataset=name)
131133
logger.info("Dataset %r already exists, skipping.", name)
132134
return existing
133-
except Exception:
134-
logger.warning(
135-
"Could not fetch dataset %r; creating it instead (if this is "
136-
"not a 'not found' error, check your Phoenix endpoint and credentials).",
135+
except ValueError:
136+
logger.debug(
137+
"Dataset %r not found; creating it.",
137138
name,
138-
exc_info=True,
139139
)
140140
return self.client.datasets.create_dataset(
141141
dataframe=df,
@@ -165,13 +165,10 @@ def _upload_one(
165165
)
166166
logger.debug("Appended %d examples to dataset %r.", len(df), name)
167167
return dataset
168-
except Exception:
169-
logger.warning(
170-
"Could not append to dataset %r; creating it instead "
171-
"(if this is not a 'not found' error, check your Phoenix "
172-
"endpoint and credentials).",
168+
except ValueError:
169+
logger.debug(
170+
"Dataset %r not found; creating it.",
173171
name,
174-
exc_info=True,
175172
)
176173
return self.client.datasets.create_dataset(
177174
dataframe=df,

tests/test_cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def test_upload_custom_input_output_keys(self, tmp_path: Path):
7575
csv_file = tmp_path / "custom.csv"
7676
csv_file.write_text("q,ans,grp\nq1,a1,g1\n")
7777
client = _mock_client()
78-
client.datasets.get_dataset.side_effect = Exception("not found")
78+
client.datasets.get_dataset.side_effect = ValueError("not found")
7979
with patch("evalwire.cli._make_client", return_value=client):
8080
result = _runner().invoke(
8181
main,

tests/test_runner.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,21 @@ def test_returns_none_on_import_error(
143143
result = runner._load_attribute(py_file, "anything")
144144
assert result is None
145145

146+
def test_cleans_sys_modules_on_import_error(
147+
self, tmp_path: Path, mock_phoenix_client: MagicMock
148+
):
149+
"""A broken module must not leak into sys.modules."""
150+
import sys
151+
152+
py_file = tmp_path / "leaky.py"
153+
py_file.write_text("raise RuntimeError('kaboom')\n")
154+
runner = _make_runner(tmp_path, mock_phoenix_client)
155+
module_name = f"_evalwire_exp_{py_file.parent.name}_{py_file.stem}"
156+
157+
result = runner._load_attribute(py_file, "anything")
158+
assert result is None
159+
assert module_name not in sys.modules
160+
146161

147162
class TestRun:
148163
def test_run_calls_run_experiment_for_each_discovered(

tests/test_uploader.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from pathlib import Path
44
from unittest.mock import MagicMock
55

6+
import pytest
7+
68
from evalwire.uploader import DatasetUploader
79

810

@@ -87,12 +89,22 @@ def test_skips_when_dataset_already_exists(
8789
def test_uploads_when_dataset_does_not_exist(
8890
self, sample_csv: Path, mock_phoenix_client: MagicMock
8991
):
90-
mock_phoenix_client.datasets.get_dataset.side_effect = Exception("not found")
92+
mock_phoenix_client.datasets.get_dataset.side_effect = ValueError("not found")
9193
uploader = _make_uploader(sample_csv, mock_phoenix_client)
9294
result = uploader.upload(on_exist="skip")
9395
assert mock_phoenix_client.datasets.create_dataset.called
9496
assert len(result) == 2
9597

98+
def test_non_not_found_error_propagates(
99+
self, sample_csv: Path, mock_phoenix_client: MagicMock
100+
):
101+
mock_phoenix_client.datasets.get_dataset.side_effect = RuntimeError(
102+
"connection refused"
103+
)
104+
uploader = _make_uploader(sample_csv, mock_phoenix_client)
105+
with pytest.raises(RuntimeError, match="connection refused"):
106+
uploader.upload(on_exist="skip")
107+
96108

97109
class TestUploadOverwrite:
98110
def test_always_calls_upload_dataset(
@@ -105,7 +117,7 @@ def test_always_calls_upload_dataset(
105117
def test_overwrite_when_dataset_missing_still_creates(
106118
self, sample_csv: Path, mock_phoenix_client: MagicMock
107119
):
108-
mock_phoenix_client.datasets.get_dataset.side_effect = Exception("not found")
120+
mock_phoenix_client.datasets.get_dataset.side_effect = ValueError("not found")
109121
uploader = _make_uploader(sample_csv, mock_phoenix_client)
110122
result = uploader.upload(on_exist="overwrite")
111123
assert mock_phoenix_client.datasets.create_dataset.called
@@ -123,7 +135,7 @@ def test_calls_append_to_dataset_when_dataset_exists(
123135
def test_falls_back_to_create_when_dataset_missing(
124136
self, sample_csv: Path, mock_phoenix_client: MagicMock
125137
):
126-
mock_phoenix_client.datasets.add_examples_to_dataset.side_effect = Exception(
138+
mock_phoenix_client.datasets.add_examples_to_dataset.side_effect = ValueError(
127139
"not found"
128140
)
129141
uploader = _make_uploader(sample_csv, mock_phoenix_client)
@@ -144,7 +156,7 @@ def test_custom_delimiter(self, tmp_path: Path, mock_phoenix_client: MagicMock):
144156
tag_column="group",
145157
delimiter=";",
146158
)
147-
mock_phoenix_client.datasets.get_dataset.side_effect = Exception("not found")
159+
mock_phoenix_client.datasets.get_dataset.side_effect = ValueError("not found")
148160
result = uploader.upload(on_exist="skip")
149161
assert "g1" in result
150162
assert "g2" in result
@@ -157,6 +169,6 @@ def test_custom_tag_column(self, tmp_path: Path, mock_phoenix_client: MagicMock)
157169
phoenix_client=mock_phoenix_client,
158170
tag_column="category",
159171
)
160-
mock_phoenix_client.datasets.get_dataset.side_effect = Exception("not found")
172+
mock_phoenix_client.datasets.get_dataset.side_effect = ValueError("not found")
161173
result = uploader.upload(on_exist="skip")
162174
assert "cat1" in result

0 commit comments

Comments
 (0)