chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import datasets
|
||||
import pyarrow
|
||||
import pytest
|
||||
import requests
|
||||
from packaging.version import Version
|
||||
|
||||
import ray
|
||||
from ray.data.dataset import Dataset, MaterializedDataset
|
||||
from ray.tests.conftest import * # noqa
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_hf_dataset():
|
||||
"""Create a mock HuggingFace dataset for testing."""
|
||||
texts = [
|
||||
"Climate change is a serious threat to our planet",
|
||||
"We need to take action on global warming",
|
||||
"Renewable energy is the future",
|
||||
"Fossil fuels are destroying the environment",
|
||||
"Solar power is becoming more affordable",
|
||||
"Wind energy is growing rapidly",
|
||||
"Electric vehicles are the way forward",
|
||||
"Carbon emissions must be reduced",
|
||||
"Green technology is advancing quickly",
|
||||
"Sustainability is important for future generations",
|
||||
"Climate science is well established",
|
||||
"Ocean levels are rising due to warming",
|
||||
"Extreme weather events are increasing",
|
||||
"Biodiversity loss is accelerating",
|
||||
"Deforestation contributes to climate change",
|
||||
"Clean energy jobs are growing",
|
||||
"Energy efficiency saves money",
|
||||
"Public transportation reduces emissions",
|
||||
"Plant-based diets help the environment",
|
||||
"Recycling is essential for sustainability",
|
||||
]
|
||||
|
||||
# Create labels array with exactly the same length as texts
|
||||
labels = [i % 2 for i in range(len(texts))] # Alternating 0s and 1s
|
||||
|
||||
return datasets.Dataset.from_dict(
|
||||
{
|
||||
"text": texts,
|
||||
"label": labels,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_hf_dataset_dict(mock_hf_dataset):
|
||||
"""Create a mock HuggingFace DatasetDict for testing."""
|
||||
return datasets.DatasetDict({"train": mock_hf_dataset})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_hf_iterable_dataset():
|
||||
"""Create a mock HuggingFace IterableDataset for testing."""
|
||||
texts = [
|
||||
"Streaming climate tweet 1: The planet is warming",
|
||||
"Streaming climate tweet 2: Renewable energy is key",
|
||||
"Streaming climate tweet 3: We must act now",
|
||||
"Streaming climate tweet 4: Solar panels everywhere",
|
||||
"Streaming climate tweet 5: Wind turbines are beautiful",
|
||||
"Streaming climate tweet 6: Electric cars are the future",
|
||||
"Streaming climate tweet 7: Carbon neutral by 2050",
|
||||
"Streaming climate tweet 8: Green energy revolution",
|
||||
"Streaming climate tweet 9: Climate action needed",
|
||||
"Streaming climate tweet 10: Sustainable development",
|
||||
"Streaming climate tweet 11: Ocean conservation",
|
||||
"Streaming climate tweet 12: Forest protection",
|
||||
"Streaming climate tweet 13: Clean air matters",
|
||||
"Streaming climate tweet 14: Water conservation",
|
||||
"Streaming climate tweet 15: Biodiversity protection",
|
||||
]
|
||||
|
||||
labels = [1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1]
|
||||
|
||||
dataset = datasets.Dataset.from_dict(
|
||||
{
|
||||
"text": texts,
|
||||
"label": labels,
|
||||
}
|
||||
)
|
||||
iterable_dataset = dataset.to_iterable_dataset()
|
||||
iterable_dataset.expected_count = len(texts)
|
||||
return iterable_dataset
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_parquet_urls():
|
||||
"""Fixture providing mock parquet URLs for testing."""
|
||||
return [
|
||||
"https://huggingface.co/datasets/test/parquet/train-00000-of-00001.parquet",
|
||||
"https://huggingface.co/datasets/test/parquet/train-00001-of-00001.parquet",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_resolved_urls():
|
||||
"""Fixture providing mock resolved URLs (after HTTP redirects) for testing."""
|
||||
return [
|
||||
"https://cdn-lfs.huggingface.co/datasets/test/parquet/train-00000-of-00001.parquet",
|
||||
"https://cdn-lfs.huggingface.co/datasets/test/parquet/train-00001-of-00001.parquet",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ray_dataset(mock_hf_dataset):
|
||||
"""Fixture providing a mock Ray dataset that matches the mock HuggingFace dataset."""
|
||||
return ray.data.from_items(
|
||||
[
|
||||
{"text": text, "label": label}
|
||||
for text, label in zip(mock_hf_dataset["text"], mock_hf_dataset["label"])
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_successful_http_responses(mock_parquet_urls):
|
||||
"""Fixture providing mock successful HTTP responses for URL resolution."""
|
||||
mock_responses = []
|
||||
for url in mock_parquet_urls:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.url = url
|
||||
mock_responses.append(mock_response)
|
||||
return mock_responses
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_redirected_http_responses(mock_parquet_urls, mock_resolved_urls):
|
||||
"""Fixture providing mock HTTP responses that simulate redirects."""
|
||||
mock_responses = []
|
||||
for original_url, resolved_url in zip(mock_parquet_urls, mock_resolved_urls):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.url = resolved_url
|
||||
mock_responses.append(mock_response)
|
||||
return mock_responses
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_huggingface_datasource():
|
||||
"""Fixture providing the HuggingFaceDatasource class for mocking."""
|
||||
from ray.data._internal.datasource.huggingface_datasource import (
|
||||
HuggingFaceDatasource,
|
||||
)
|
||||
|
||||
return HuggingFaceDatasource
|
||||
|
||||
|
||||
def verify_http_requests(mock_requests_head, expected_urls):
|
||||
"""Verify that HTTP requests were made correctly."""
|
||||
assert mock_requests_head.call_count == len(expected_urls)
|
||||
|
||||
for i, url in enumerate(expected_urls):
|
||||
call_args = mock_requests_head.call_args_list[i]
|
||||
assert call_args[0][0] == url
|
||||
assert call_args[1]["allow_redirects"] is True
|
||||
assert call_args[1]["timeout"] == 5
|
||||
|
||||
|
||||
def verify_read_parquet_call(mock_read_parquet, expected_urls):
|
||||
"""Verify that read_parquet was called with correct parameters."""
|
||||
mock_read_parquet.assert_called_once()
|
||||
call_args = mock_read_parquet.call_args
|
||||
|
||||
# Check that the parquet URLs were passed
|
||||
assert call_args[0][0] == expected_urls
|
||||
|
||||
# Check that the filesystem is HTTPFileSystem
|
||||
assert "filesystem" in call_args[1]
|
||||
assert "HTTPFileSystem" in str(type(call_args[1]["filesystem"]))
|
||||
|
||||
# Check that retry_exceptions includes FileNotFoundError and ClientResponseError
|
||||
assert "ray_remote_args" in call_args[1]
|
||||
assert FileNotFoundError in call_args[1]["ray_remote_args"]["retry_exceptions"]
|
||||
|
||||
|
||||
def verify_dataset_creation(ds, mock_hf_dataset):
|
||||
"""Verify that the dataset was created successfully."""
|
||||
assert isinstance(ds, MaterializedDataset)
|
||||
assert ds.count() == mock_hf_dataset.num_rows
|
||||
|
||||
|
||||
def setup_parquet_mocks(
|
||||
mock_huggingface_datasource,
|
||||
mock_parquet_urls,
|
||||
mock_http_responses,
|
||||
mock_ray_dataset,
|
||||
):
|
||||
"""Setup common mocking pattern for parquet-based tests."""
|
||||
patches = []
|
||||
|
||||
# Mock the list_parquet_urls_from_dataset method
|
||||
datasource_patch = patch.object(
|
||||
mock_huggingface_datasource,
|
||||
"list_parquet_urls_from_dataset",
|
||||
return_value=mock_parquet_urls,
|
||||
)
|
||||
patches.append(datasource_patch)
|
||||
|
||||
# Mock the requests.head calls
|
||||
requests_patch = patch("requests.head")
|
||||
patches.append(requests_patch)
|
||||
|
||||
# Mock the read_parquet function
|
||||
read_parquet_patch = patch("ray.data.read_api.read_parquet")
|
||||
patches.append(read_parquet_patch)
|
||||
|
||||
# Start all patches
|
||||
datasource_mock = datasource_patch.start()
|
||||
requests_mock = requests_patch.start()
|
||||
read_parquet_mock = read_parquet_patch.start()
|
||||
|
||||
# Configure mocks
|
||||
requests_mock.side_effect = mock_http_responses
|
||||
read_parquet_mock.return_value = mock_ray_dataset
|
||||
|
||||
return datasource_mock, requests_mock, read_parquet_mock, patches
|
||||
|
||||
|
||||
def hfds_assert_equals(hfds: datasets.Dataset, ds: Dataset):
|
||||
hfds_table = hfds.data.table
|
||||
ds_table = pyarrow.concat_tables([ray.get(tbl) for tbl in ds.to_arrow_refs()])
|
||||
|
||||
sorting = [(name, "descending") for name in hfds_table.column_names]
|
||||
hfds_table = hfds_table.sort_by(sorting)
|
||||
ds_table = ds_table.sort_by(sorting)
|
||||
|
||||
assert hfds_table.equals(ds_table)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_par", [1, 4])
|
||||
def test_from_huggingface(mock_hf_dataset_dict, ray_start_regular_shared, num_par):
|
||||
# Check that DatasetDict is not directly supported.
|
||||
assert isinstance(mock_hf_dataset_dict, datasets.DatasetDict)
|
||||
with pytest.raises(
|
||||
DeprecationWarning,
|
||||
match="You provided a Hugging Face DatasetDict",
|
||||
):
|
||||
ray.data.from_huggingface(mock_hf_dataset_dict)
|
||||
|
||||
ray_datasets = {
|
||||
"train": ray.data.from_huggingface(
|
||||
mock_hf_dataset_dict["train"], override_num_blocks=num_par
|
||||
),
|
||||
}
|
||||
|
||||
assert isinstance(ray_datasets["train"], ray.data.Dataset)
|
||||
hfds_assert_equals(mock_hf_dataset_dict["train"], ray_datasets["train"])
|
||||
|
||||
# Test reading in a split Hugging Face dataset yields correct individual datasets
|
||||
base_hf_dataset = mock_hf_dataset_dict["train"]
|
||||
hf_dataset_split = base_hf_dataset.train_test_split(test_size=0.2)
|
||||
ray_dataset_split_train = ray.data.from_huggingface(hf_dataset_split["train"])
|
||||
assert ray_dataset_split_train.count() == hf_dataset_split["train"].num_rows
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
datasets.Version(datasets.__version__) < datasets.Version("2.8.0"),
|
||||
reason="IterableDataset.iter() added in 2.8.0",
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
Version(pyarrow.__version__) < Version("8.0.0"),
|
||||
reason="pyarrow.Table.to_reader() added in 8.0.0",
|
||||
)
|
||||
# Note, pandas is excluded here because IterableDatasets do not support pandas format.
|
||||
@pytest.mark.parametrize(
|
||||
"batch_format",
|
||||
[None, "numpy", "arrow", "torch", "tensorflow", "jax"],
|
||||
)
|
||||
def test_from_huggingface_streaming(
|
||||
mock_hf_iterable_dataset, batch_format, ray_start_regular_shared
|
||||
):
|
||||
hfds = mock_hf_iterable_dataset.with_format(batch_format)
|
||||
assert isinstance(hfds, datasets.IterableDataset)
|
||||
|
||||
ds = ray.data.from_huggingface(hfds)
|
||||
expected_count = mock_hf_iterable_dataset.expected_count
|
||||
assert ds.count() == expected_count
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
datasets.Version(datasets.__version__) < datasets.Version("2.8.0"),
|
||||
reason="IterableDataset.iter() added in 2.8.0",
|
||||
)
|
||||
def test_from_huggingface_dynamic_generated(ray_start_regular_shared):
|
||||
# https://github.com/ray-project/ray/issues/49529
|
||||
# Mock the dynamic dataset loading
|
||||
mock_dataset = datasets.Dataset.from_dict(
|
||||
{
|
||||
"text": [
|
||||
"dynamic tweet 1",
|
||||
"dynamic tweet 2",
|
||||
"dynamic tweet 3",
|
||||
"dynamic tweet 4",
|
||||
"dynamic tweet 5",
|
||||
],
|
||||
"label": [0, 1, 0, 1, 0],
|
||||
}
|
||||
)
|
||||
mock_iterable = mock_dataset.to_iterable_dataset()
|
||||
|
||||
with patch("datasets.load_dataset", return_value=mock_iterable):
|
||||
hfds = datasets.load_dataset(
|
||||
"dataset-org/dream",
|
||||
split="test",
|
||||
streaming=True,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
ds = ray.data.from_huggingface(hfds)
|
||||
ds.take(1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("override_num_blocks", [1, 2, 4, 8])
|
||||
def test_from_huggingface_override_num_blocks(
|
||||
mock_hf_dataset, ray_start_regular_shared, override_num_blocks
|
||||
):
|
||||
"""Test that override_num_blocks works correctly with HuggingFace datasets."""
|
||||
hf_train = mock_hf_dataset
|
||||
|
||||
ds_subset = ray.data.from_huggingface(
|
||||
hf_train, override_num_blocks=override_num_blocks
|
||||
)
|
||||
|
||||
assert isinstance(ds_subset, MaterializedDataset)
|
||||
|
||||
# Verify number of blocks
|
||||
assert ds_subset.num_blocks() == override_num_blocks
|
||||
|
||||
# Verify data integrity
|
||||
assert ds_subset.count() == hf_train.num_rows
|
||||
hfds_assert_equals(hf_train, ds_subset)
|
||||
|
||||
# Test with a smaller subset to test edge cases
|
||||
small_size = max(override_num_blocks * 3, 10)
|
||||
hf_small = hf_train.select(range(min(small_size, hf_train.num_rows)))
|
||||
ds_small = ray.data.from_huggingface(
|
||||
hf_small, override_num_blocks=override_num_blocks
|
||||
)
|
||||
|
||||
# Verify number of blocks
|
||||
assert ds_small.num_blocks() == override_num_blocks
|
||||
|
||||
# Verify data integrity
|
||||
assert ds_small.count() == hf_small.num_rows
|
||||
hfds_assert_equals(hf_small, ds_small)
|
||||
|
||||
|
||||
def test_from_huggingface_with_parquet_files(
|
||||
mock_hf_dataset,
|
||||
ray_start_regular_shared,
|
||||
mock_parquet_urls,
|
||||
mock_ray_dataset,
|
||||
mock_successful_http_responses,
|
||||
mock_huggingface_datasource,
|
||||
):
|
||||
"""Test the distributed read path when parquet file URLs are available."""
|
||||
datasource_mock, requests_mock, read_parquet_mock, patches = setup_parquet_mocks(
|
||||
mock_huggingface_datasource,
|
||||
mock_parquet_urls,
|
||||
mock_successful_http_responses,
|
||||
mock_ray_dataset,
|
||||
)
|
||||
|
||||
try:
|
||||
ds = ray.data.from_huggingface(mock_hf_dataset)
|
||||
|
||||
# Verify HTTP requests
|
||||
verify_http_requests(requests_mock, mock_parquet_urls)
|
||||
|
||||
# Verify read_parquet call
|
||||
verify_read_parquet_call(read_parquet_mock, mock_parquet_urls)
|
||||
|
||||
# Verify dataset creation
|
||||
verify_dataset_creation(ds, mock_hf_dataset)
|
||||
|
||||
finally:
|
||||
# Stop all patches
|
||||
for patch_obj in patches:
|
||||
patch_obj.stop()
|
||||
|
||||
|
||||
def test_from_huggingface_with_resolved_urls(
|
||||
mock_hf_dataset,
|
||||
ray_start_regular_shared,
|
||||
mock_parquet_urls,
|
||||
mock_resolved_urls,
|
||||
mock_ray_dataset,
|
||||
mock_redirected_http_responses,
|
||||
mock_huggingface_datasource,
|
||||
):
|
||||
"""Test the URL resolution logic when HTTP redirects are encountered."""
|
||||
datasource_mock, requests_mock, read_parquet_mock, patches = setup_parquet_mocks(
|
||||
mock_huggingface_datasource,
|
||||
mock_parquet_urls,
|
||||
mock_redirected_http_responses,
|
||||
mock_ray_dataset,
|
||||
)
|
||||
|
||||
try:
|
||||
ds = ray.data.from_huggingface(mock_hf_dataset)
|
||||
|
||||
# Verify HTTP requests
|
||||
verify_http_requests(requests_mock, mock_parquet_urls)
|
||||
|
||||
# Verify read_parquet call with resolved URLs
|
||||
verify_read_parquet_call(read_parquet_mock, mock_resolved_urls)
|
||||
|
||||
# Verify dataset creation
|
||||
verify_dataset_creation(ds, mock_hf_dataset)
|
||||
|
||||
finally:
|
||||
# Stop all patches
|
||||
for patch_obj in patches:
|
||||
patch_obj.stop()
|
||||
|
||||
|
||||
def test_from_huggingface_url_resolution_failures(
|
||||
mock_hf_dataset,
|
||||
ray_start_regular_shared,
|
||||
mock_parquet_urls,
|
||||
mock_ray_dataset,
|
||||
mock_huggingface_datasource,
|
||||
):
|
||||
"""Test URL resolution failures fall back to single node read."""
|
||||
# Convert the mock dataset to an IterableDataset so it uses the read_datasource fallback
|
||||
mock_iterable_dataset = mock_hf_dataset.to_iterable_dataset()
|
||||
|
||||
with patch.object(
|
||||
mock_huggingface_datasource,
|
||||
"list_parquet_urls_from_dataset",
|
||||
return_value=mock_parquet_urls,
|
||||
):
|
||||
# Mock the requests.head calls to simulate failures
|
||||
with patch("requests.head") as mock_requests_head:
|
||||
# Configure mock to raise an exception for all URLs
|
||||
mock_requests_head.side_effect = requests.RequestException(
|
||||
"Connection failed"
|
||||
)
|
||||
|
||||
# Mock the fallback path
|
||||
with patch("ray.data.read_api.read_datasource") as mock_read_datasource:
|
||||
mock_read_datasource.return_value = mock_ray_dataset
|
||||
|
||||
ds = ray.data.from_huggingface(mock_iterable_dataset)
|
||||
|
||||
# Verify that requests.head was called for each URL
|
||||
assert mock_requests_head.call_count == len(mock_parquet_urls)
|
||||
|
||||
# Verify that the fallback read_datasource was called
|
||||
mock_read_datasource.assert_called_once()
|
||||
|
||||
# Verify the dataset was created successfully via fallback
|
||||
verify_dataset_creation(ds, mock_hf_dataset)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
Reference in New Issue
Block a user