Files

258 lines
11 KiB
Python

from unittest import mock
import pyarrow
import pyarrow.fs
import pytest
from fsspec.implementations.http import HTTPFileSystem
from pyarrow.fs import FSSpecHandler, PyFileSystem
from ray.data._internal.util import RetryingPyFileSystem
from ray.data.datasource.path_util import (
_has_file_extension,
_is_filesystem_compatible_with_scheme,
_is_local_windows_path,
_resolve_paths_and_filesystem,
_resolve_single_path_with_fallback,
_rewrite_azure_blob_https_url,
)
from ray.util.annotations import RayDeprecationWarning
@pytest.mark.parametrize(
"path, extensions, has_extension",
[
("foo.csv", ["csv"], True),
("foo.csv", ["json", "csv"], True),
("foo.csv", ["json", "jsonl"], False),
("foo.csv", [".csv"], True),
("foo.parquet.crc", ["parquet"], False),
("foo.parquet.crc", ["crc"], True),
("s3://bucket/foo.parquet?versionId=abc123", ["parquet"], True),
("bucket/foo.parquet?versionId=abc123", ["parquet"], True),
("s3://bucket/foo.parquet?versionId=abc123", ["csv"], False),
("s3://bucket/data#v2/file.parquet", ["parquet"], True),
("C:\\data\\test.parquet", ["parquet"], True),
("C:\\data\\parquet", ["parquet"], False),
("foo.csv", None, True),
],
)
def test_has_file_extension(path, extensions, has_extension):
assert _has_file_extension(path, extensions) == has_extension
@pytest.mark.parametrize(
"filesystem", [None, PyFileSystem(FSSpecHandler(HTTPFileSystem()))]
)
def test_resolve_http_paths(filesystem):
resolved_paths, resolve_filesystem = _resolve_paths_and_filesystem(
"https://google.com", filesystem
)
# `_resolve_paths_and_filesystem` shouldn't remove the protocol/scheme from the
# path for HTTP paths.
assert resolved_paths == ["https://google.com"]
assert isinstance(resolve_filesystem, pyarrow.fs.PyFileSystem)
assert isinstance(resolve_filesystem.handler, pyarrow.fs.FSSpecHandler)
assert isinstance(resolve_filesystem.handler.fs, HTTPFileSystem)
@pytest.mark.parametrize(
"path, expected",
[
# Blob endpoint: rewritten to PyArrow-compatible bare-container form.
(
"https://acct.blob.core.windows.net/cont/dir/file.jpg",
("abfs://cont/dir/file.jpg", "acct"),
),
# DFS / Gen2 endpoint: same target form, account still extracted.
(
"https://acct.dfs.core.windows.net/cont/dir/file.jpg",
("abfs://cont/dir/file.jpg", "acct"),
),
# Mixed-case host is still recognised; account is lower-cased.
(
"https://Acct.Blob.Core.Windows.Net/cont/file.bin",
("abfs://cont/file.bin", "acct"),
),
# Explicit :443 port — host suffix match still works via parsed.hostname.
(
"https://acct.blob.core.windows.net:443/cont/dir/file.jpg",
("abfs://cont/dir/file.jpg", "acct"),
),
# Userinfo prefix (`user:pass@host`) — also stripped by parsed.hostname.
(
"https://user:pass@acct.blob.core.windows.net/cont/file.bin",
("abfs://cont/file.bin", "acct"),
),
# SAS token present — leave URL untouched so HTTP path serves it.
(
"https://acct.blob.core.windows.net/cont/file?sv=2024&sig=abc",
("https://acct.blob.core.windows.net/cont/file?sv=2024&sig=abc", None),
),
# Non-Azure HTTPS host — passthrough.
("https://example.com/cont/file", ("https://example.com/cont/file", None)),
# Container with no blob path — not enough to rewrite.
(
"https://acct.blob.core.windows.net/cont",
("https://acct.blob.core.windows.net/cont", None),
),
# Bare account URL with no container.
(
"https://acct.blob.core.windows.net/",
("https://acct.blob.core.windows.net/", None),
),
# Already-abfs URI is unchanged.
(
"abfs://cont/dir/file",
("abfs://cont/dir/file", None),
),
# Non-HTTPS scheme is unchanged.
("s3://bucket/key", ("s3://bucket/key", None)),
],
)
def test_rewrite_azure_blob_https_url(path, expected):
assert _rewrite_azure_blob_https_url(path) == expected
def test_resolve_single_path_without_azure_filesystem_keeps_https_passthrough():
"""When PyArrow has no AzureFileSystem (older builds), an Azure Blob HTTPS
URL must NOT be rewritten to abfs:// — otherwise the resolver would have
no filesystem to construct, the HTTP fallback below cannot match the new
scheme, and resolution fails outright. The URL should pass through to the
fsspec HTTP filesystem unchanged.
"""
import pyarrow.fs
url = "https://acct.blob.core.windows.net/cont/dir/file.bin"
# The production code reads pyarrow.fs.AzureFileSystem via
# ``getattr(pyarrow.fs, "AzureFileSystem", None)`` and treats ``None`` as
# "not available", so patching the attribute to None simulates the
# missing-Azure-support build without needing to delete it.
with mock.patch.object(pyarrow.fs, "AzureFileSystem", None):
fs, resolved = _resolve_single_path_with_fallback(url)
assert resolved == url
assert isinstance(fs, pyarrow.fs.PyFileSystem)
assert isinstance(fs.handler, pyarrow.fs.FSSpecHandler)
assert isinstance(fs.handler.fs, HTTPFileSystem)
@pytest.mark.parametrize(
"path",
[
"c:/some/where",
"c:\\some\\where",
"c:\\some\\where/mixed",
],
)
def test_windows_path(path):
with mock.patch("sys.platform", "win32"):
assert _is_local_windows_path(path)
@pytest.mark.parametrize(
"path",
[
"some/file",
"some/file;semicolon",
"some/file?questionmark",
"some/file#hash",
"some/file;all?of the#above",
],
)
def test_weird_local_paths(path):
resolved_paths, _ = _resolve_paths_and_filesystem(path)
assert resolved_paths[0] == path
@pytest.mark.parametrize("prefix", ["local://", "local:"])
def test_resolve_local_scheme_emits_deprecation_warning(tmp_path, prefix):
path = tmp_path / "data.txt"
path.write_text("")
with pytest.warns(RayDeprecationWarning, match="local://"):
_resolve_paths_and_filesystem(f"{prefix}{path}")
class TestIsFilesystemCompatibleWithScheme:
"""Tests for _is_filesystem_compatible_with_scheme with real filesystem implementations."""
def test_native_local_filesystem(self):
"""Native PyArrow LocalFileSystem should be compatible with empty scheme."""
fs = pyarrow.fs.LocalFileSystem()
assert _is_filesystem_compatible_with_scheme(fs, "") is True
assert _is_filesystem_compatible_with_scheme(fs, "s3") is False
def test_native_s3_filesystem(self):
"""Native PyArrow S3FileSystem should be compatible with s3 scheme."""
fs = pyarrow.fs.S3FileSystem(anonymous=True, region="us-east-1")
assert _is_filesystem_compatible_with_scheme(fs, "s3") is True
assert _is_filesystem_compatible_with_scheme(fs, "") is False
def test_fsspec_s3_filesystem_with_s3_scheme(self):
"""fsspec S3FileSystem wrapped in PyFileSystem should be compatible with s3 scheme."""
s3fs = pytest.importorskip("s3fs")
wrapped_fs = PyFileSystem(FSSpecHandler(s3fs.S3FileSystem(anon=True)))
assert _is_filesystem_compatible_with_scheme(wrapped_fs, "s3") is True
def test_fsspec_s3_filesystem_with_bare_paths(self):
"""fsspec S3FileSystem wrapped in PyFileSystem should be compatible with bare paths."""
s3fs = pytest.importorskip("s3fs")
wrapped_fs = PyFileSystem(FSSpecHandler(s3fs.S3FileSystem(anon=True)))
assert _is_filesystem_compatible_with_scheme(wrapped_fs, "") is True
def test_fsspec_s3_filesystem_with_s3a_scheme(self):
"""Real s3fs has protocol=('s3', 's3a'), so it should match s3a scheme."""
s3fs = pytest.importorskip("s3fs")
wrapped_fs = PyFileSystem(FSSpecHandler(s3fs.S3FileSystem(anon=True)))
assert _is_filesystem_compatible_with_scheme(wrapped_fs, "s3a") is True
def test_fsspec_gcs_filesystem_with_gs_scheme(self):
"""fsspec GCS filesystem should be compatible with gs scheme."""
gcsfs = pytest.importorskip("gcsfs")
wrapped_fs = PyFileSystem(FSSpecHandler(gcsfs.GCSFileSystem(token="anon")))
assert _is_filesystem_compatible_with_scheme(wrapped_fs, "gs") is True
def test_fsspec_gcs_filesystem_with_gcs_scheme(self):
"""Real gcsfs has protocol=('gcs', 'gs'), so it should match gcs scheme."""
gcsfs = pytest.importorskip("gcsfs")
wrapped_fs = PyFileSystem(FSSpecHandler(gcsfs.GCSFileSystem(token="anon")))
assert _is_filesystem_compatible_with_scheme(wrapped_fs, "gcs") is True
def test_fsspec_http_filesystem(self):
"""fsspec HTTPFileSystem wrapped in PyFileSystem should be compatible with http."""
wrapped_fs = PyFileSystem(FSSpecHandler(HTTPFileSystem()))
assert _is_filesystem_compatible_with_scheme(wrapped_fs, "http") is True
assert _is_filesystem_compatible_with_scheme(wrapped_fs, "https") is True
def test_fsspec_s3_not_compatible_with_gs(self):
"""fsspec S3FileSystem should NOT be compatible with gs scheme."""
s3fs = pytest.importorskip("s3fs")
wrapped_fs = PyFileSystem(FSSpecHandler(s3fs.S3FileSystem(anon=True)))
assert _is_filesystem_compatible_with_scheme(wrapped_fs, "gs") is False
def test_fsspec_s3_not_compatible_with_http(self):
"""fsspec S3FileSystem should NOT be compatible with http/https schemes."""
s3fs = pytest.importorskip("s3fs")
wrapped_fs = PyFileSystem(FSSpecHandler(s3fs.S3FileSystem(anon=True)))
assert _is_filesystem_compatible_with_scheme(wrapped_fs, "http") is False
assert _is_filesystem_compatible_with_scheme(wrapped_fs, "https") is False
def test_retrying_wrapper_around_native_s3(self):
"""RetryingPyFileSystem wrapping a native S3FileSystem should be compatible with s3."""
s3_fs = pyarrow.fs.S3FileSystem(anonymous=True)
retrying_fs = RetryingPyFileSystem.wrap(s3_fs, retryable_errors=["AWS Error"])
assert _is_filesystem_compatible_with_scheme(retrying_fs, "s3") is True
assert _is_filesystem_compatible_with_scheme(retrying_fs, "gs") is False
def test_unknown_scheme_trusts_filesystem(self):
"""Unknown schemes should always return True (trust user's filesystem)."""
fs = pyarrow.fs.LocalFileSystem()
assert _is_filesystem_compatible_with_scheme(fs, "custom") is True
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))