Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions pyrit/datasets/seed_datasets/local/local_dataset_loader.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

import asyncio
import logging
from collections.abc import Callable
from dataclasses import fields
Expand Down Expand Up @@ -68,7 +69,7 @@ async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset:
"""
try:
logger.info(f"Loading local dataset from {self.file_path}")
dataset = SeedDataset.from_yaml_file(self.file_path)
dataset = await asyncio.to_thread(SeedDataset.from_yaml_file, self.file_path)
if not dataset.dataset_name:
dataset.dataset_name = self.dataset_name
return dataset
Expand All @@ -91,8 +92,7 @@ async def _parse_metadata_async(self) -> SeedDatasetMetadata | None:
"""
valid_fields = [f.name for f in fields(SeedDatasetMetadata)]
try:
with open(self.file_path, encoding="utf-8") as f:
dataset = yaml.safe_load(f)
dataset = await asyncio.to_thread(self._read_yaml)
except Exception as e:
logger.error(f"Failed to load local dataset from {self.file_path}: {e}")
raise
Expand All @@ -111,6 +111,15 @@ async def _parse_metadata_async(self) -> SeedDatasetMetadata | None:
SeedDatasetMetadata._validate_singular_fields(metadata=result)
return result

def _read_yaml(self) -> Any:
"""
Read and parse the local dataset YAML file.

Returns:
Any: Parsed YAML content.
"""
return yaml.safe_load(self.file_path.read_text(encoding="utf-8"))


def _register_local_datasets() -> None:
"""
Expand Down
56 changes: 55 additions & 1 deletion tests/unit/datasets/test_local_dataset_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
# Licensed under the MIT license.

from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from pyrit.datasets.seed_datasets.local.local_dataset_loader import _LocalDatasetLoader
from pyrit.models import SeedDataset
from pyrit.models import SeedDataset, SeedPrompt


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

async def test_fetch_dataset_offloads_file_read(self, tmp_path: Path) -> None:
"""Dataset file loading runs outside the event loop thread."""
file_path = tmp_path / "test.yaml"
loader = _LocalDatasetLoader.__new__(_LocalDatasetLoader)
loader.file_path = file_path
loader._dataset_name = "test_dataset"
expected = SeedDataset(
dataset_name="test_dataset",
seeds=[SeedPrompt(value="test prompt", data_type="text")],
)
to_thread_mock = AsyncMock(return_value=expected)

with (
patch.object(SeedDataset, "from_yaml_file") as load_mock,
patch(
"pyrit.datasets.seed_datasets.local.local_dataset_loader.asyncio.to_thread",
new=to_thread_mock,
),
):
dataset = await loader.fetch_dataset_async()

assert dataset is expected
to_thread_mock.assert_awaited_once_with(load_mock, file_path)
load_mock.assert_not_called()

async def test_parse_metadata_offloads_file_read(self, tmp_path: Path) -> None:
"""Metadata YAML parsing runs outside the event loop thread."""
file_path = tmp_path / "test.yaml"
loader = _LocalDatasetLoader.__new__(_LocalDatasetLoader)
loader.file_path = file_path
loader._dataset_name = "test_dataset"
read_yaml_mock = MagicMock(
return_value={
"dataset_name": "test_dataset",
"harm_categories": ["violence"],
}
)
to_thread_mock = AsyncMock(return_value=read_yaml_mock.return_value)

with (
patch.object(loader, "_read_yaml", new=read_yaml_mock),
patch(
"pyrit.datasets.seed_datasets.local.local_dataset_loader.asyncio.to_thread",
new=to_thread_mock,
),
):
metadata = await loader._parse_metadata_async()

assert metadata is not None
assert metadata.harm_categories == {"violence"}
to_thread_mock.assert_awaited_once_with(read_yaml_mock)
read_yaml_mock.assert_not_called()

async def test_fetch_dataset_file_not_found(self):
loader = _LocalDatasetLoader(file_path=Path("non_existent.yaml"))
with pytest.raises(Exception):
Expand Down