Skip to content

Commit ef22ca1

Browse files
committed
Address review comments for PR fsspec#786
1 parent 625ef11 commit ef22ca1

File tree

3 files changed

+147
-72
lines changed

3 files changed

+147
-72
lines changed

gcsfs/core.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,15 +1088,15 @@ async def _info(self, path, generation=None, **kwargs):
10881088
self._get_directory_info(path, bucket, key, generation),
10891089
]
10901090
) as (tasks, done, pending):
1091-
exact_task, dir_task = tasks
1091+
get_object_task, get_directory_info_task = tasks
10921092

10931093
try:
1094-
exact_res = await exact_task
1094+
exact_res = await get_object_task
10951095
if not _is_directory_marker(exact_res):
10961096
return exact_res
10971097
except FileNotFoundError:
10981098
pass
1099-
return await dir_task
1099+
return await get_directory_info_task
11001100

11011101
async def _get_directory_info(self, path, bucket, key, generation):
11021102
"""

gcsfs/tests/test_concurrency.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import asyncio
2+
3+
import pytest
4+
5+
from gcsfs.concurrency import parallel_tasks_first_completed
6+
7+
8+
@pytest.mark.asyncio
9+
async def test_parallel_tasks_first_completed_basic():
10+
async def slow_task():
11+
await asyncio.sleep(1)
12+
return "slow"
13+
14+
async def fast_task():
15+
await asyncio.sleep(0.1)
16+
return "fast"
17+
18+
async with parallel_tasks_first_completed([slow_task(), fast_task()]) as (
19+
tasks,
20+
done,
21+
pending,
22+
):
23+
assert len(done) == 1
24+
assert len(pending) == 1
25+
completed_task = done.pop()
26+
assert completed_task.result() == "fast"
27+
assert len(tasks) == 2
28+
29+
30+
@pytest.mark.asyncio
31+
async def test_parallel_tasks_first_completed_cancellation():
32+
task_cancelled = False
33+
34+
async def slow_task():
35+
nonlocal task_cancelled
36+
try:
37+
await asyncio.sleep(1)
38+
except asyncio.CancelledError:
39+
task_cancelled = True
40+
raise
41+
42+
async def fast_task():
43+
await asyncio.sleep(0.1)
44+
return "fast"
45+
46+
async with parallel_tasks_first_completed([slow_task(), fast_task()]) as (
47+
tasks,
48+
done,
49+
pending,
50+
):
51+
assert len(done) == 1
52+
completed_task = done.pop()
53+
assert completed_task.result() == "fast"
54+
55+
# After exiting context, slow_task should be cancelled
56+
await asyncio.sleep(0.1) # Give it a moment to run cancellation cleanup
57+
assert task_cancelled
58+
59+
60+
@pytest.mark.asyncio
61+
async def test_parallel_tasks_first_completed_exception():
62+
async def error_task():
63+
await asyncio.sleep(0.1)
64+
raise ValueError("error")
65+
66+
async def slow_task():
67+
await asyncio.sleep(1)
68+
return "slow"
69+
70+
async with parallel_tasks_first_completed([error_task(), slow_task()]) as (
71+
tasks,
72+
done,
73+
pending,
74+
):
75+
assert len(done) == 1
76+
completed_task = done.pop()
77+
with pytest.raises(ValueError, match="error"):
78+
completed_task.result()

gcsfs/tests/test_core.py

Lines changed: 66 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2005,7 +2005,55 @@ def test_mv_file_raises_error_for_specific_generation(gcs):
20052005

20062006

20072007
@pytest.mark.asyncio
2008-
async def test_info_parallel(gcs):
2008+
@pytest.mark.parametrize(
2009+
"object_behavior, dir_behavior, expected",
2010+
[
2011+
(
2012+
{"return": {"name": TEST_BUCKET + "/file", "type": "file", "size": 100}},
2013+
{"exception": FileNotFoundError},
2014+
{"return": {"type": "file"}},
2015+
),
2016+
(
2017+
{"exception": FileNotFoundError},
2018+
{"return": {"name": TEST_BUCKET + "/file", "type": "directory", "size": 0}},
2019+
{"return": {"type": "directory"}},
2020+
),
2021+
(
2022+
{
2023+
"return": {
2024+
"name": TEST_BUCKET + "/file/",
2025+
"type": "directory",
2026+
"size": 0,
2027+
}
2028+
},
2029+
{
2030+
"return": {
2031+
"name": TEST_BUCKET + "/file",
2032+
"type": "directory",
2033+
"size": 0,
2034+
"extra": "info",
2035+
}
2036+
},
2037+
{"return": {"type": "directory", "extra": "info"}},
2038+
),
2039+
(
2040+
{"exception": Exception("Generic error")},
2041+
{"exception": FileNotFoundError},
2042+
{"exception": Exception, "match": "Generic error"},
2043+
),
2044+
(
2045+
{"exception": FileNotFoundError},
2046+
{"exception": Exception("Directory error")},
2047+
{"exception": Exception, "match": "Directory error"},
2048+
),
2049+
(
2050+
{"exception": FileNotFoundError},
2051+
{"exception": FileNotFoundError},
2052+
{"exception": FileNotFoundError},
2053+
),
2054+
],
2055+
)
2056+
async def test_info_parallel(gcs, object_behavior, dir_behavior, expected):
20092057
path = TEST_BUCKET + "/file"
20102058

20112059
with (
@@ -2017,77 +2065,26 @@ async def test_info_parallel(gcs):
20172065
) as mock_get_dir,
20182066
):
20192067

2020-
# Case 1: File exists. _get_object succeeds, _get_directory_info fails.
2021-
mock_get_object.return_value = {"name": path, "type": "file", "size": 100}
2022-
mock_get_dir.side_effect = FileNotFoundError
2068+
if "return" in object_behavior:
2069+
mock_get_object.return_value = object_behavior["return"]
2070+
elif "exception" in object_behavior:
2071+
mock_get_object.side_effect = object_behavior["exception"]
20232072

2024-
res = await gcs._info(path)
2025-
assert res["type"] == "file"
2026-
assert mock_get_object.call_count == 1
2027-
assert mock_get_dir.call_count == 1
2073+
if "return" in dir_behavior:
2074+
mock_get_dir.return_value = dir_behavior["return"]
2075+
elif "exception" in dir_behavior:
2076+
mock_get_dir.side_effect = dir_behavior["exception"]
20282077

2029-
# Case 2: Directory. _get_object fails, _get_directory_info succeeds.
2030-
mock_get_object.reset_mock()
2031-
mock_get_dir.reset_mock()
2032-
mock_get_object.side_effect = FileNotFoundError
2033-
mock_get_dir.side_effect = None
2034-
mock_get_dir.return_value = {"name": path, "type": "directory", "size": 0}
2035-
2036-
res = await gcs._info(path)
2037-
assert res["type"] == "directory"
2038-
assert mock_get_object.call_count == 1
2039-
assert mock_get_dir.call_count == 1
2040-
2041-
# Case 3: Directory marker. _get_object returns marker, _get_directory_info returns dir info.
2042-
mock_get_object.reset_mock()
2043-
mock_get_dir.reset_mock()
2044-
mock_get_object.side_effect = None
2045-
# A directory marker has size 0 and ends with /
2046-
marker_path = path + "/"
2047-
mock_get_object.return_value = {
2048-
"name": marker_path,
2049-
"type": "directory",
2050-
"size": 0,
2051-
}
2052-
mock_get_dir.return_value = {
2053-
"name": path,
2054-
"type": "directory",
2055-
"size": 0,
2056-
"extra": "info",
2057-
}
2058-
2059-
res = await gcs._info(path)
2060-
# Should prefer directory info over marker
2061-
assert res["extra"] == "info"
2062-
assert mock_get_object.call_count == 1
2063-
assert mock_get_dir.call_count == 1
2064-
2065-
# Case 4: _get_object raises generic exception. Should raise immediately.
2066-
mock_get_object.reset_mock()
2067-
mock_get_dir.reset_mock()
2068-
mock_get_object.side_effect = Exception("Generic error")
2069-
mock_get_dir.side_effect = FileNotFoundError
2070-
2071-
with pytest.raises(Exception, match="Generic error"):
2072-
await gcs._info(path)
2073-
2074-
# Case 5: _get_object fails with FileNotFoundError, _get_directory_info raises generic exception.
2075-
mock_get_object.reset_mock()
2076-
mock_get_dir.reset_mock()
2077-
mock_get_object.side_effect = FileNotFoundError
2078-
mock_get_dir.side_effect = Exception("Directory error")
2079-
2080-
with pytest.raises(Exception, match="Directory error"):
2081-
await gcs._info(path)
2082-
2083-
# Case 6: Both fail with FileNotFoundError. Should raise FileNotFoundError.
2084-
mock_get_object.reset_mock()
2085-
mock_get_dir.reset_mock()
2086-
mock_get_object.side_effect = FileNotFoundError
2087-
mock_get_dir.side_effect = FileNotFoundError
2078+
if "exception" in expected:
2079+
with pytest.raises(expected["exception"], match=expected.get("match")):
2080+
await gcs._info(path)
2081+
else:
2082+
res = await gcs._info(path)
2083+
for k, v in expected["return"].items():
2084+
assert res[k] == v
20882085

2089-
with pytest.raises(FileNotFoundError):
2090-
await gcs._info(path)
2086+
assert mock_get_object.call_count == 1
2087+
assert mock_get_dir.call_count == 1
20912088

20922089

20932090
@pytest.mark.asyncio

0 commit comments

Comments
 (0)