Skip to content

Commit f8733a5

Browse files
committed
Refactor tests to use pytest.mark.parametrize per reviewer feedback
Replace class-based test structure with flat parametrized functions to match the repository's testing conventions.
1 parent c3a97f6 commit f8733a5

1 file changed

Lines changed: 107 additions & 268 deletions

File tree

python/tests/test_object_store_param.py

Lines changed: 107 additions & 268 deletions
Original file line numberDiff line numberDiff line change
@@ -22,280 +22,119 @@
2222
from unittest.mock import MagicMock, patch
2323

2424
import pyarrow as pa
25+
import pyarrow.parquet as pq
2526
import pytest
2627
from datafusion import SessionContext
28+
from datafusion.object_store import LocalFileSystem
2729

2830

2931
@pytest.fixture
3032
def ctx():
3133
return SessionContext()
3234

3335

34-
class TestRegisterObjectStoreForPath:
35-
"""Unit tests for _register_object_store_for_path URL parsing logic."""
36-
37-
def test_parses_s3_url(self, ctx):
38-
mock_store = MagicMock()
39-
with patch.object(ctx, "register_object_store") as mock_register:
40-
ctx._register_object_store_for_path(
41-
"s3://my-bucket/path/to/file.parquet", mock_store
42-
)
43-
mock_register.assert_called_once_with("s3://", mock_store, host="my-bucket")
44-
45-
def test_parses_gs_url(self, ctx):
46-
mock_store = MagicMock()
47-
with patch.object(ctx, "register_object_store") as mock_register:
48-
ctx._register_object_store_for_path(
49-
"gs://my-gcs-bucket/data.parquet", mock_store
50-
)
51-
mock_register.assert_called_once_with(
52-
"gs://", mock_store, host="my-gcs-bucket"
53-
)
54-
55-
def test_parses_az_url(self, ctx):
56-
mock_store = MagicMock()
57-
with patch.object(ctx, "register_object_store") as mock_register:
58-
ctx._register_object_store_for_path(
59-
"az://my-container/data.parquet", mock_store
60-
)
61-
mock_register.assert_called_once_with(
62-
"az://", mock_store, host="my-container"
63-
)
64-
65-
def test_parses_https_url(self, ctx):
66-
mock_store = MagicMock()
67-
with patch.object(ctx, "register_object_store") as mock_register:
68-
ctx._register_object_store_for_path(
69-
"https://my-host.example.com/data.parquet", mock_store
70-
)
71-
mock_register.assert_called_once_with(
72-
"https://", mock_store, host="my-host.example.com"
73-
)
74-
75-
def test_raises_on_local_path(self, ctx):
76-
mock_store = MagicMock()
77-
with pytest.raises(ValueError, match="Cannot determine object store URL"):
78-
ctx._register_object_store_for_path("/local/path/file.parquet", mock_store)
79-
80-
def test_raises_on_relative_path(self, ctx):
81-
mock_store = MagicMock()
82-
with pytest.raises(ValueError, match="Cannot determine object store URL"):
83-
ctx._register_object_store_for_path("relative/path.parquet", mock_store)
84-
85-
def test_raises_on_windows_path(self, ctx):
86-
mock_store = MagicMock()
87-
with pytest.raises(ValueError, match="must include a host or bucket"):
88-
ctx._register_object_store_for_path(
89-
"C:\\Users\\data\\file.parquet", mock_store
90-
)
91-
92-
def test_raises_on_scheme_without_host_for_non_file(self, ctx):
93-
"""Non-file schemes (s3, gs, etc.) require a host/bucket."""
94-
mock_store = MagicMock()
95-
with pytest.raises(ValueError, match="must include a host or bucket"):
96-
ctx._register_object_store_for_path("s3:///key.parquet", mock_store)
97-
98-
def test_parses_file_url(self, ctx):
99-
"""file:// URLs with empty netloc should be accepted."""
100-
mock_store = MagicMock()
101-
with patch.object(ctx, "register_object_store") as mock_register:
102-
ctx._register_object_store_for_path(
103-
"file:///tmp/path/to/file.parquet", mock_store
104-
)
105-
mock_register.assert_called_once_with("file://", mock_store, host=None)
106-
107-
def test_accepts_pathlib_path_raises(self, ctx):
108-
"""pathlib.Path cannot represent URLs, so this should raise."""
109-
mock_store = MagicMock()
110-
# pathlib.Path strips the scheme, so this becomes a local path
111-
with pytest.raises(ValueError, match="Cannot determine object store URL"):
112-
ctx._register_object_store_for_path(Path("/local/file.parquet"), mock_store)
113-
114-
115-
class TestRegisterParquetObjectStore:
116-
"""Tests for register_parquet with object_store parameter."""
117-
118-
def test_object_store_none_does_not_register(self, ctx):
119-
"""When object_store is None, register_object_store is not called."""
120-
with patch.object(ctx, "register_object_store") as mock_register:
121-
# This will fail at the Rust level (file doesn't exist), but
122-
# we're testing that register_object_store is NOT called
123-
with contextlib.suppress(Exception):
124-
ctx.register_parquet("t", "s3://bucket/file.parquet")
125-
mock_register.assert_not_called()
126-
127-
def test_object_store_triggers_registration(self, ctx):
128-
"""When object_store is provided, register_object_store is called."""
129-
mock_store = MagicMock()
130-
with patch.object(ctx, "register_object_store") as mock_register:
131-
with contextlib.suppress(Exception):
132-
ctx.register_parquet(
133-
"t",
134-
"s3://my-bucket/file.parquet",
135-
object_store=mock_store,
136-
)
137-
mock_register.assert_called_once_with("s3://", mock_store, host="my-bucket")
138-
139-
def test_object_store_invalid_path_raises(self, ctx):
140-
"""Providing object_store with a local path raises ValueError."""
141-
mock_store = MagicMock()
142-
with pytest.raises(ValueError, match="Cannot determine object store URL"):
143-
ctx.register_parquet("t", "/local/file.parquet", object_store=mock_store)
144-
145-
146-
class TestReadParquetObjectStore:
147-
"""Tests for read_parquet with object_store parameter."""
148-
149-
def test_object_store_triggers_registration(self, ctx):
150-
mock_store = MagicMock()
151-
with patch.object(ctx, "register_object_store") as mock_register:
152-
with contextlib.suppress(Exception):
153-
ctx.read_parquet("s3://my-bucket/file.parquet", object_store=mock_store)
154-
mock_register.assert_called_once_with("s3://", mock_store, host="my-bucket")
155-
156-
157-
class TestRegisterCsvObjectStore:
158-
"""Tests for register_csv with object_store parameter."""
159-
160-
def test_object_store_triggers_registration(self, ctx):
161-
mock_store = MagicMock()
162-
with patch.object(ctx, "register_object_store") as mock_register:
163-
with contextlib.suppress(Exception):
164-
ctx.register_csv(
165-
"t", "s3://my-bucket/data.csv", object_store=mock_store
166-
)
167-
mock_register.assert_called_once_with("s3://", mock_store, host="my-bucket")
168-
169-
def test_object_store_with_list_path(self, ctx):
170-
"""For list paths, the first entry is used for URL parsing."""
171-
mock_store = MagicMock()
172-
with patch.object(ctx, "register_object_store") as mock_register:
173-
with contextlib.suppress(Exception):
174-
ctx.register_csv(
175-
"t",
176-
["s3://my-bucket/a.csv", "s3://my-bucket/b.csv"],
177-
object_store=mock_store,
178-
)
179-
mock_register.assert_called_once_with("s3://", mock_store, host="my-bucket")
180-
181-
182-
class TestReadCsvObjectStore:
183-
"""Tests for read_csv with object_store parameter."""
184-
185-
def test_object_store_triggers_registration(self, ctx):
186-
mock_store = MagicMock()
187-
with patch.object(ctx, "register_object_store") as mock_register:
188-
with contextlib.suppress(Exception):
189-
ctx.read_csv("gs://bucket/data.csv", object_store=mock_store)
190-
mock_register.assert_called_once_with("gs://", mock_store, host="bucket")
191-
192-
193-
class TestRegisterJsonObjectStore:
194-
"""Tests for register_json with object_store parameter."""
195-
196-
def test_object_store_triggers_registration(self, ctx):
197-
mock_store = MagicMock()
198-
with patch.object(ctx, "register_object_store") as mock_register:
199-
with contextlib.suppress(Exception):
200-
ctx.register_json("t", "s3://bucket/data.json", object_store=mock_store)
201-
mock_register.assert_called_once_with("s3://", mock_store, host="bucket")
202-
203-
204-
class TestReadJsonObjectStore:
205-
"""Tests for read_json with object_store parameter."""
206-
207-
def test_object_store_triggers_registration(self, ctx):
208-
mock_store = MagicMock()
209-
with patch.object(ctx, "register_object_store") as mock_register:
210-
with contextlib.suppress(Exception):
211-
ctx.read_json("s3://bucket/data.json", object_store=mock_store)
212-
mock_register.assert_called_once_with("s3://", mock_store, host="bucket")
213-
214-
215-
class TestRegisterAvroObjectStore:
216-
"""Tests for register_avro with object_store parameter."""
217-
218-
def test_object_store_triggers_registration(self, ctx):
219-
mock_store = MagicMock()
220-
with patch.object(ctx, "register_object_store") as mock_register:
221-
with contextlib.suppress(Exception):
222-
ctx.register_avro("t", "s3://bucket/data.avro", object_store=mock_store)
223-
mock_register.assert_called_once_with("s3://", mock_store, host="bucket")
224-
225-
226-
class TestReadAvroObjectStore:
227-
"""Tests for read_avro with object_store parameter."""
228-
229-
def test_object_store_triggers_registration(self, ctx):
230-
mock_store = MagicMock()
231-
with patch.object(ctx, "register_object_store") as mock_register:
232-
with contextlib.suppress(Exception):
233-
ctx.read_avro("s3://bucket/data.avro", object_store=mock_store)
234-
mock_register.assert_called_once_with("s3://", mock_store, host="bucket")
235-
236-
237-
class TestRegisterArrowObjectStore:
238-
"""Tests for register_arrow with object_store parameter."""
239-
240-
def test_object_store_triggers_registration(self, ctx):
241-
mock_store = MagicMock()
242-
with patch.object(ctx, "register_object_store") as mock_register:
243-
with contextlib.suppress(Exception):
244-
ctx.register_arrow(
245-
"t", "s3://bucket/data.arrow", object_store=mock_store
246-
)
247-
mock_register.assert_called_once_with("s3://", mock_store, host="bucket")
248-
249-
250-
class TestReadArrowObjectStore:
251-
"""Tests for read_arrow with object_store parameter."""
252-
253-
def test_object_store_triggers_registration(self, ctx):
254-
mock_store = MagicMock()
255-
with patch.object(ctx, "register_object_store") as mock_register:
256-
with contextlib.suppress(Exception):
257-
ctx.read_arrow("s3://bucket/data.arrow", object_store=mock_store)
258-
mock_register.assert_called_once_with("s3://", mock_store, host="bucket")
259-
260-
261-
class TestEndToEndWithLocalFileSystem:
262-
"""Integration test using LocalFileSystem object store with register_parquet."""
263-
264-
def test_register_parquet_with_local_object_store(self, ctx, tmp_path):
265-
"""Verify the full flow works with a real object store and local file."""
266-
import pyarrow.parquet as pq
267-
from datafusion.object_store import LocalFileSystem
268-
269-
# Write a test parquet file
270-
table = pa.table({"x": [1, 2, 3], "y": ["a", "b", "c"]})
271-
parquet_path = tmp_path / "test.parquet"
272-
pq.write_table(table, str(parquet_path))
273-
274-
# Use file:// URL with LocalFileSystem object store
275-
store = LocalFileSystem()
276-
file_url = parquet_path.as_uri()
277-
278-
ctx.register_parquet("test_tbl", file_url, object_store=store)
279-
result = ctx.sql("SELECT * FROM test_tbl").collect()
280-
281-
assert len(result) == 1
282-
assert result[0].num_rows == 3
283-
assert result[0].column("x").to_pylist() == [1, 2, 3]
284-
285-
def test_read_parquet_with_local_object_store(self, ctx, tmp_path):
286-
"""Verify read_parquet works with object_store parameter."""
287-
import pyarrow.parquet as pq
288-
from datafusion.object_store import LocalFileSystem
289-
290-
table = pa.table({"val": [10, 20, 30]})
291-
parquet_path = tmp_path / "read_test.parquet"
292-
pq.write_table(table, str(parquet_path))
293-
294-
store = LocalFileSystem()
295-
file_url = parquet_path.as_uri()
296-
297-
df = ctx.read_parquet(file_url, object_store=store)
298-
result = df.collect()
299-
300-
assert len(result) == 1
301-
assert result[0].column("val").to_pylist() == [10, 20, 30]
36+
@pytest.mark.parametrize(
37+
("path", "scheme", "host"),
38+
[
39+
("s3://my-bucket/path/file.parquet", "s3://", "my-bucket"),
40+
("gs://my-gcs-bucket/data.parquet", "gs://", "my-gcs-bucket"),
41+
("az://my-container/data.parquet", "az://", "my-container"),
42+
("https://example.com/data.parquet", "https://", "example.com"),
43+
("file:///tmp/data.parquet", "file://", None),
44+
],
45+
)
46+
def test_register_object_store_for_url(ctx, path, scheme, host):
47+
store = MagicMock()
48+
49+
with patch.object(ctx, "register_object_store") as register:
50+
ctx._register_object_store_for_path(path, store)
51+
52+
register.assert_called_once_with(scheme, store, host=host)
53+
54+
55+
@pytest.mark.parametrize(
56+
("path", "error"),
57+
[
58+
("/local/path/file.parquet", "Cannot determine object store URL"),
59+
("relative/path.parquet", "Cannot determine object store URL"),
60+
(Path("/local/file.parquet"), "Cannot determine object store URL"),
61+
("C:\\Users\\data\\file.parquet", "must include a host or bucket"),
62+
("s3:///key.parquet", "must include a host or bucket"),
63+
],
64+
)
65+
def test_register_object_store_rejects_invalid_url(path, error, ctx):
66+
with pytest.raises(ValueError, match=error):
67+
ctx._register_object_store_for_path(path, MagicMock())
68+
69+
70+
@pytest.mark.parametrize(
71+
("method_name", "args", "path"),
72+
[
73+
("register_parquet", ("table",), "s3://bucket/data.parquet"),
74+
("read_parquet", (), "s3://bucket/data.parquet"),
75+
("register_csv", ("table",), "s3://bucket/data.csv"),
76+
("read_csv", (), "s3://bucket/data.csv"),
77+
("register_json", ("table",), "s3://bucket/data.json"),
78+
("read_json", (), "s3://bucket/data.json"),
79+
("register_avro", ("table",), "s3://bucket/data.avro"),
80+
("read_avro", (), "s3://bucket/data.avro"),
81+
("register_arrow", ("table",), "s3://bucket/data.arrow"),
82+
("read_arrow", (), "s3://bucket/data.arrow"),
83+
],
84+
)
85+
def test_file_methods_register_object_store(ctx, method_name, args, path):
86+
store = MagicMock()
87+
88+
# The remote file does not exist. Registration happens before DataFusion
89+
# tries to inspect it, which is the behavior under test.
90+
with (
91+
patch.object(ctx, "register_object_store") as register,
92+
contextlib.suppress(Exception),
93+
):
94+
getattr(ctx, method_name)(*args, path, object_store=store)
95+
96+
register.assert_called_once_with("s3://", store, host="bucket")
97+
98+
99+
def test_register_csv_uses_first_path_for_object_store(ctx):
100+
store = MagicMock()
101+
paths = ["s3://bucket/a.csv", "s3://bucket/b.csv"]
102+
103+
with (
104+
patch.object(ctx, "register_object_store") as register,
105+
contextlib.suppress(Exception),
106+
):
107+
ctx.register_csv("table", paths, object_store=store)
108+
109+
register.assert_called_once_with("s3://", store, host="bucket")
110+
111+
112+
def test_object_store_none_does_not_register(ctx):
113+
with (
114+
patch.object(ctx, "register_object_store") as register,
115+
contextlib.suppress(Exception),
116+
):
117+
ctx.register_parquet("table", "missing.parquet")
118+
119+
register.assert_not_called()
120+
121+
122+
def test_file_method_rejects_local_path_with_object_store(ctx):
123+
with pytest.raises(ValueError, match="Cannot determine object store URL"):
124+
ctx.register_parquet("table", "/local/file.parquet", object_store=MagicMock())
125+
126+
127+
@pytest.mark.parametrize("method_name", ["register_parquet", "read_parquet"])
128+
def test_parquet_methods_with_local_object_store(ctx, tmp_path, method_name):
129+
table = pa.table({"value": [10, 20, 30]})
130+
parquet_path = tmp_path / "data.parquet"
131+
pq.write_table(table, parquet_path)
132+
133+
path = parquet_path.as_uri()
134+
if method_name == "register_parquet":
135+
ctx.register_parquet("test_table", path, object_store=LocalFileSystem())
136+
dataframe = ctx.sql("SELECT * FROM test_table")
137+
else:
138+
dataframe = ctx.read_parquet(path, object_store=LocalFileSystem())
139+
140+
assert dataframe.collect()[0].column("value").to_pylist() == [10, 20, 30]

0 commit comments

Comments
 (0)