# Copyright NVIDIA Corporation 2023 # SPDX-License-Identifier: Apache-2.0 import glob import io import os import pickle import tarfile import pytest import webdataset as wds import ray from ray.tests.conftest import * # noqa class TarWriter: def __init__(self, path): self.path = path self.tar = tarfile.open(path, "w") def __enter__(self): return self def __exit__(self, *args): self.tar.close() def write(self, name, data): f = self.tar.tarinfo() f.name = name f.size = len(data) self.tar.addfile(f, io.BytesIO(data)) def test_webdataset_read(ray_start_2_cpus, tmp_path): path = os.path.join(tmp_path, "bar_000000.tar") with TarWriter(path) as tf: for i in range(100): tf.write(f"{i}.a", str(i).encode("utf-8")) tf.write(f"{i}.b", str(i**2).encode("utf-8")) assert os.path.exists(path) assert len(glob.glob(f"{tmp_path}/*.tar")) == 1 ds = ray.data.read_webdataset(paths=[str(tmp_path)]) samples = ds.take(100) assert len(samples) == 100 for i, sample in enumerate(samples): assert isinstance(sample, dict), sample assert sample["__key__"] == str(i) assert sample["a"].decode("utf-8") == str(i) assert sample["b"].decode("utf-8") == str(i**2) @pytest.fixture def allow_unsafe_deserialization(monkeypatch): monkeypatch.setenv("RAY_DATA_WEBDATASET_ALLOW_UNSAFE_DESERIALIZATION", "1") def test_webdataset_expand_json( ray_start_2_cpus, tmp_path, allow_unsafe_deserialization ): import numpy as np import torch image = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8) gray = np.random.randint(0, 255, (100, 100), dtype=np.uint8) dstruct = dict(a=[1, 2], b=dict(c=2), d="hello") ttensor = torch.tensor([1, 2, 3]).numpy() sample = { "__key__": "foo", "jpg": image, "gray.png": gray, "mp": dstruct, "json": dstruct, "pt": ttensor, "und": b"undecoded", "custom": b"nothing", } # write the encoded data using the default encoder data = [sample] ds = ray.data.from_items(data).repartition(1) ds.write_webdataset(path=tmp_path, try_create_dir=True) ds = ray.data.read_webdataset( paths=[str(tmp_path)], override_num_blocks=1, expand_json=True ) record = ds.take(1) assert [1, 2] == record[0]["a"] def test_webdataset_suffixes(ray_start_2_cpus, tmp_path): path = os.path.join(tmp_path, "bar_000000.tar") with TarWriter(path) as tf: for i in range(100): tf.write(f"{i}.txt", str(i).encode("utf-8")) tf.write(f"{i}.test.txt", str(i**2).encode("utf-8")) tf.write(f"{i}.cls", str(i**2).encode("utf-8")) tf.write(f"{i}.test.cls2", str(i**2).encode("utf-8")) assert os.path.exists(path) assert len(glob.glob(f"{tmp_path}/*.tar")) == 1 # test simple suffixes ds = ray.data.read_webdataset(paths=[str(tmp_path)], suffixes=["txt", "cls"]) samples = ds.take(100) assert len(samples) == 100 for i, sample in enumerate(samples): assert set(sample.keys()) == {"__url__", "__key__", "txt", "cls"} # test fnmatch patterns for suffixes ds = ray.data.read_webdataset(paths=[str(tmp_path)], suffixes=["*.txt", "*.cls"]) samples = ds.take(100) assert len(samples) == 100 for i, sample in enumerate(samples): assert set(sample.keys()) == {"__url__", "__key__", "txt", "cls", "test.txt"} # test selection function def select(name): return name.endswith("txt") ds = ray.data.read_webdataset(paths=[str(tmp_path)], suffixes=select) samples = ds.take(100) assert len(samples) == 100 for i, sample in enumerate(samples): assert set(sample.keys()) == {"__url__", "__key__", "txt", "test.txt"} # test filerename def renamer(name): result = name.replace("txt", "text") print("***", name, result) return result ds = ray.data.read_webdataset(paths=[str(tmp_path)], filerename=renamer) samples = ds.take(100) assert len(samples) == 100 for i, sample in enumerate(samples): assert set(sample.keys()) == { "__url__", "__key__", "text", "cls", "test.text", "test.cls2", } def test_webdataset_write(ray_start_2_cpus, tmp_path): print(ray.available_resources()) data = [dict(__key__=str(i), a=str(i), b=str(i**2)) for i in range(100)] ds = ray.data.from_items(data).repartition(1) ds.write_webdataset(path=tmp_path, try_create_dir=True) paths = glob.glob(f"{tmp_path}/*.tar") assert len(paths) == 1 with open(paths[0], "rb") as stream: tf = tarfile.open(fileobj=stream) for i in range(100): assert tf.extractfile(f"{i}.a").read().decode("utf-8") == str(i) assert tf.extractfile(f"{i}.b").read().decode("utf-8") == str(i**2) def custom_decoder(sample): for key, value in sample.items(): if key == "png": # check that images have already been decoded assert not isinstance(value, bytes) elif key.endswith("custom"): sample[key] = "custom-value" return sample def test_webdataset_coding(ray_start_2_cpus, tmp_path, allow_unsafe_deserialization): import numpy as np import PIL.Image import torch image = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8) gray = np.random.randint(0, 255, (100, 100), dtype=np.uint8) dstruct = dict(a=[1], b=dict(c=2), d="hello") ttensor = torch.tensor([1, 2, 3]).numpy() sample = { "__key__": "foo", "jpg": image, "gray.png": gray, "mp": dstruct, "json": dstruct, "pt": ttensor, "und": b"undecoded", "custom": b"nothing", } # write the encoded data using the default encoder data = [sample] ds = ray.data.from_items(data).repartition(1) ds.write_webdataset(path=tmp_path, try_create_dir=True) # read the encoded data using the default decoder paths = glob.glob(f"{tmp_path}/*.tar") assert len(paths) == 1 path = paths[0] assert os.path.exists(path) ds = ray.data.read_webdataset(paths=[str(tmp_path)]) samples = ds.take(1) assert len(samples) == 1 for sample in samples: assert isinstance(sample, dict), sample assert sample["__key__"] == "foo" assert isinstance(sample["jpg"], np.ndarray) assert sample["jpg"].shape == (100, 100, 3) assert isinstance(sample["gray.png"], np.ndarray) assert sample["gray.png"].shape == (100, 100) assert isinstance(sample["mp"], dict) assert sample["mp"]["a"] == [1] assert sample["mp"]["b"]["c"] == 2 assert isinstance(sample["json"], dict) assert sample["json"]["a"] == [1] assert isinstance(sample["pt"], np.ndarray) assert sample["pt"].tolist() == [1, 2, 3] # test the format argument to the default decoder and multiple decoders ds = ray.data.read_webdataset( paths=[str(tmp_path)], decoder=["PIL", custom_decoder] ) samples = ds.take(1) assert len(samples) == 1 for sample in samples: assert isinstance(sample, dict), sample assert sample["__key__"] == "foo" assert isinstance(sample["jpg"], PIL.Image.Image) assert isinstance(sample["gray.png"], PIL.Image.Image) assert isinstance(sample["und"], bytes) assert sample["und"] == b"undecoded" assert sample["custom"] == "custom-value" def test_webdataset_decoding(ray_start_2_cpus, tmp_path): import numpy as np import torch image = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8) gray = np.random.randint(0, 255, (100, 100), dtype=np.uint8) dstruct = dict(a=np.nan, b=dict(c=2), d="hello", e={"img_filename": "for_test.jpg"}) ttensor = torch.tensor([1, 2, 3]).numpy() sample = { "__key__": "foo", "jpg": image, "gray.png": gray, "mp": dstruct, "json": dstruct, "pt": ttensor, "und": b"undecoded", "custom": b"nothing", } # write the encoded data using the default encoder data = [sample] ds = ray.data.from_items(data).repartition(1) ds.write_webdataset(path=tmp_path, try_create_dir=True) ds = ray.data.read_webdataset( paths=[str(tmp_path)], override_num_blocks=1, decoder=None, ) samples = ds.take(1) import json meta_json = json.loads(samples[0]["json"].decode("utf-8")) assert meta_json["e"]["img_filename"] == "for_test.jpg" @pytest.mark.parametrize("min_rows_per_file", [5, 10, 50]) def test_write_min_rows_per_file(tmp_path, ray_start_2_cpus, min_rows_per_file): ray.data.from_items( [{"id": str(i)} for i in range(100)], override_num_blocks=20 ).write_webdataset(tmp_path, min_rows_per_file=min_rows_per_file) for filename in os.listdir(tmp_path): dataset = wds.WebDataset(os.path.join(tmp_path, filename)) assert len(list(dataset)) == min_rows_per_file @pytest.mark.parametrize( "filename", ["000000.pkl", "000000.pickle", "000000.pt", "000000.pth"], ) def test_default_decoder_rejects_unsafe_extensions( ray_start_2_cpus, tmp_path, filename ): path = os.path.join(tmp_path, "unsafe.tar") with TarWriter(path) as tf: tf.write(filename, b"fake-payload") ds = ray.data.read_webdataset(paths=[str(tmp_path)]) with pytest.raises(Exception, match="Refusing to"): ds.take_all() def test_default_decoder_allows_unsafe_with_env_var( ray_start_2_cpus, tmp_path, allow_unsafe_deserialization ): path = os.path.join(tmp_path, "trusted.tar") with TarWriter(path) as tf: tf.write("000000.pkl", pickle.dumps({"key": "value"})) ds = ray.data.read_webdataset(paths=[str(tmp_path)]) rows = ds.take_all() assert len(rows) == 1 assert rows[0]["pkl"] == {"key": "value"} def test_custom_decoder_bypasses_unsafe_guard(ray_start_2_cpus, tmp_path): path = os.path.join(tmp_path, "custom.tar") with TarWriter(path) as tf: tf.write("000000.pkl", pickle.dumps({"key": "value"})) def safe_pkl_decoder(sample): sample = dict(sample) for key, value in sample.items(): if key == "pkl": sample[key] = pickle.loads(value) return sample ds = ray.data.read_webdataset(paths=[str(tmp_path)], decoder=safe_pkl_decoder) rows = ds.take_all() assert len(rows) == 1 assert rows[0]["pkl"] == {"key": "value"} if __name__ == "__main__": import sys sys.exit(pytest.main(["-v", __file__]))