Skip to content

Commit 1270f26

Browse files
FIX: Offload local dataset file reads (#2402)
Co-authored-by: hannahwestra25 <hannahwestra@microsoft.com>
1 parent a75e851 commit 1270f26

2 files changed

Lines changed: 67 additions & 4 deletions

File tree

pyrit/datasets/seed_datasets/local/local_dataset_loader.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT license.
33

4+
import asyncio
45
import logging
56
from collections.abc import Callable
67
from dataclasses import fields
@@ -68,7 +69,7 @@ async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset:
6869
"""
6970
try:
7071
logger.info(f"Loading local dataset from {self.file_path}")
71-
dataset = SeedDataset.from_yaml_file(self.file_path)
72+
dataset = await asyncio.to_thread(SeedDataset.from_yaml_file, self.file_path)
7273
if not dataset.dataset_name:
7374
dataset.dataset_name = self.dataset_name
7475
return dataset
@@ -91,8 +92,7 @@ async def _parse_metadata_async(self) -> SeedDatasetMetadata | None:
9192
"""
9293
valid_fields = [f.name for f in fields(SeedDatasetMetadata)]
9394
try:
94-
with open(self.file_path, encoding="utf-8") as f:
95-
dataset = yaml.safe_load(f)
95+
dataset = await asyncio.to_thread(self._read_yaml)
9696
except Exception as e:
9797
logger.error(f"Failed to load local dataset from {self.file_path}: {e}")
9898
raise
@@ -111,6 +111,15 @@ async def _parse_metadata_async(self) -> SeedDatasetMetadata | None:
111111
SeedDatasetMetadata._validate_singular_fields(metadata=result)
112112
return result
113113

114+
def _read_yaml(self) -> Any:
115+
"""
116+
Read and parse the local dataset YAML file.
117+
118+
Returns:
119+
Any: Parsed YAML content.
120+
"""
121+
return yaml.safe_load(self.file_path.read_text(encoding="utf-8"))
122+
114123

115124
def _register_local_datasets() -> None:
116125
"""

tests/unit/datasets/test_local_dataset_loader.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
# Licensed under the MIT license.
33

44
from pathlib import Path
5+
from unittest.mock import AsyncMock, MagicMock, patch
56

67
import pytest
78

89
from pyrit.datasets.seed_datasets.local.local_dataset_loader import _LocalDatasetLoader
9-
from pyrit.models import SeedDataset
10+
from pyrit.models import SeedDataset, SeedPrompt
1011

1112

1213
class TestLocalDatasetLoader:
@@ -49,6 +50,59 @@ async def test_fetch_dataset(self, tmp_path, valid_yaml_content):
4950
assert len(dataset.prompts) == 1
5051
assert dataset.prompts[0].value == "test prompt"
5152

53+
async def test_fetch_dataset_offloads_file_read(self, tmp_path: Path) -> None:
54+
"""Dataset file loading runs outside the event loop thread."""
55+
file_path = tmp_path / "test.yaml"
56+
loader = _LocalDatasetLoader.__new__(_LocalDatasetLoader)
57+
loader.file_path = file_path
58+
loader._dataset_name = "test_dataset"
59+
expected = SeedDataset(
60+
dataset_name="test_dataset",
61+
seeds=[SeedPrompt(value="test prompt", data_type="text")],
62+
)
63+
to_thread_mock = AsyncMock(return_value=expected)
64+
65+
with (
66+
patch.object(SeedDataset, "from_yaml_file") as load_mock,
67+
patch(
68+
"pyrit.datasets.seed_datasets.local.local_dataset_loader.asyncio.to_thread",
69+
new=to_thread_mock,
70+
),
71+
):
72+
dataset = await loader.fetch_dataset_async()
73+
74+
assert dataset is expected
75+
to_thread_mock.assert_awaited_once_with(load_mock, file_path)
76+
load_mock.assert_not_called()
77+
78+
async def test_parse_metadata_offloads_file_read(self, tmp_path: Path) -> None:
79+
"""Metadata YAML parsing runs outside the event loop thread."""
80+
file_path = tmp_path / "test.yaml"
81+
loader = _LocalDatasetLoader.__new__(_LocalDatasetLoader)
82+
loader.file_path = file_path
83+
loader._dataset_name = "test_dataset"
84+
read_yaml_mock = MagicMock(
85+
return_value={
86+
"dataset_name": "test_dataset",
87+
"harm_categories": ["violence"],
88+
}
89+
)
90+
to_thread_mock = AsyncMock(return_value=read_yaml_mock.return_value)
91+
92+
with (
93+
patch.object(loader, "_read_yaml", new=read_yaml_mock),
94+
patch(
95+
"pyrit.datasets.seed_datasets.local.local_dataset_loader.asyncio.to_thread",
96+
new=to_thread_mock,
97+
),
98+
):
99+
metadata = await loader._parse_metadata_async()
100+
101+
assert metadata is not None
102+
assert metadata.harm_categories == {"violence"}
103+
to_thread_mock.assert_awaited_once_with(read_yaml_mock)
104+
read_yaml_mock.assert_not_called()
105+
52106
async def test_fetch_dataset_file_not_found(self):
53107
loader = _LocalDatasetLoader(file_path=Path("non_existent.yaml"))
54108
with pytest.raises(Exception):

0 commit comments

Comments
 (0)