Skip to content

Commit 6de847d

Browse files
authored
Merge pull request #46 from bcdev/forman-45-fix_lock_path
Fix #45
2 parents 8883fcf + d88269c commit 6de847d

File tree

7 files changed

+124
-44
lines changed

7 files changed

+124
-44
lines changed

CHANGES.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
## Version 0.3.1 (in development)
22

3+
### Fixes
4+
5+
* Fixed an issue where an absolute lock file path was computed if the target
6+
Zarr path was relative in the local filesystem, and had no parent directory.
7+
[#45]
38

49
## Version 0.3.0 (from 2024-01-26)
510

tests/fsutil/test_fileobj.py

+30-12
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,15 @@ def test_file_protocol(self):
8080
self.assertEqual(None, zarr_dir.storage_options)
8181
self.assertIsInstance(zarr_dir.fs, fsspec.AbstractFileSystem)
8282
self.assertEqual("file", to_protocol(zarr_dir.fs))
83-
self.assertEqual(os.path.abspath("test.zarr").replace("\\", "/"), zarr_dir.path)
83+
self.assertEqual(to_abs("test.zarr"), zarr_dir.path)
8484

8585
def test_local_protocol(self):
8686
zarr_dir = FileObj("test.zarr")
8787
self.assertEqual("test.zarr", zarr_dir.uri)
8888
self.assertEqual(None, zarr_dir.storage_options)
8989
self.assertIsInstance(zarr_dir.fs, fsspec.AbstractFileSystem)
9090
self.assertEqual("file", to_protocol(zarr_dir.fs))
91-
self.assertEqual(os.path.abspath("test.zarr").replace("\\", "/"), zarr_dir.path)
91+
self.assertEqual(to_abs("test.zarr"), zarr_dir.path)
9292

9393
def test_s3_protocol(self):
9494
zarr_dir = FileObj("s3://eo-data/test.zarr")
@@ -131,7 +131,7 @@ def test_truediv_override(self):
131131
"eo-data/test.zarr/chl/.zarray",
132132
)
133133

134-
def test_parent(self):
134+
def test_parent_with_uri(self):
135135
file = FileObj("s3://eo-data/test.zarr/.zmetadata")
136136
fs = file.fs
137137

@@ -157,15 +157,26 @@ def test_parent(self):
157157
# noinspection PyUnusedLocal
158158
parent = parent.parent
159159

160+
def test_parent_with_local(self):
160161
# local filesystem
161162
file = FileObj("test.zarr/chl/.zarray")
162163
fs = file.fs
163164
parent = file.parent
164165
self.assertIsInstance(parent, FileObj)
165166
self.assertEqual("test.zarr/chl", parent.uri)
166-
self.assertEqual(
167-
os.path.abspath("test.zarr/chl").replace("\\", "/"), parent.path
168-
)
167+
self.assertEqual(to_abs("test.zarr/chl"), parent.path)
168+
self.assertIs(fs, parent.fs)
169+
170+
parent = parent.parent
171+
self.assertIsInstance(parent, FileObj)
172+
self.assertEqual("test.zarr", parent.uri)
173+
self.assertEqual(to_abs("test.zarr"), parent.path)
174+
self.assertIs(fs, parent.fs)
175+
176+
parent = parent.parent
177+
self.assertIsInstance(parent, FileObj)
178+
self.assertEqual("", parent.uri)
179+
self.assertEqual(to_abs(""), parent.path)
169180
self.assertIs(fs, parent.fs)
170181

171182
def test_parent_with_chained_uri(self):
@@ -183,12 +194,10 @@ def test_parent_with_chained_uri(self):
183194
parent = file.parent
184195
self.assertIsInstance(parent, FileObj)
185196
self.assertEqual("test.zarr/chl::/eo-data/test.zarr", parent.uri)
186-
self.assertEqual(
187-
os.path.abspath("test.zarr/chl").replace("\\", "/"), parent.path
188-
)
197+
self.assertEqual(to_abs("test.zarr/chl"), parent.path)
189198
self.assertIs(fs, parent.fs)
190199

191-
def test_for_path(self):
200+
def test_for_path_with_simple_uri(self):
192201
root = FileObj("s3://eo-data/test.zarr")
193202

194203
derived = root.for_path("")
@@ -208,6 +217,11 @@ def test_for_path_with_chained_uri(self):
208217
root, derived, "dir://chl/.zarray::file:/eo-data/test.zarr", "chl/.zarray"
209218
)
210219

220+
def test_for_path_with_empty_parent(self):
221+
root = FileObj("test.zarr").parent
222+
derived = root.for_path("test.zarr")
223+
self.assert_derived_ok(root, derived, "test.zarr", to_abs("test.zarr"))
224+
211225
def assert_derived_ok(
212226
self, root: FileObj, derived: FileObj, expected_uri: str, expected_path: str
213227
):
@@ -217,13 +231,13 @@ def assert_derived_ok(
217231
self.assertIs(root.storage_options, derived.storage_options)
218232

219233
# noinspection PyMethodMayBeStatic
220-
def test_for_path_with_abs_path(self):
234+
def test_raises_for_path_with_abs_path(self):
221235
fo = FileObj("file:/eo-data/test.zarr")
222236
with pytest.raises(ValueError, match="rel_path must be relative"):
223237
fo.for_path("/test-2.zarr")
224238

225239
# noinspection PyMethodMayBeStatic
226-
def test_for_path_with_wrong_type(self):
240+
def test_raises_for_path_with_wrong_type(self):
227241
fo = FileObj("file:/eo-data/test.zarr")
228242
with pytest.raises(TypeError, match="rel_path must have type str"):
229243
# noinspection PyTypeChecker
@@ -282,3 +296,7 @@ def to_protocol(fs: fsspec.AbstractFileSystem):
282296
if isinstance(fs.protocol, tuple):
283297
return fs.protocol[0]
284298
return fs.protocol
299+
300+
301+
def to_abs(path: str) -> str:
302+
return os.path.abspath(path).replace("\\", "/")

tests/fsutil/test_path.py

+21-21
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,33 @@
77
import pytest
88

99
from zappend.fsutil.path import split_components
10-
from zappend.fsutil.path import split_filename
10+
from zappend.fsutil.path import split_parent
1111

1212

1313
class SplitFilenameTest(unittest.TestCase):
1414
# noinspection PyMethodMayBeStatic
1515
def test_empty_path(self):
1616
with pytest.raises(ValueError):
17-
split_filename("")
18-
19-
def test_split_filename_1c(self):
20-
self.assertEqual(("", ""), split_filename("/"))
21-
self.assertEqual(("", "a"), split_filename("a"))
22-
self.assertEqual(("", "a"), split_filename("/a"))
23-
self.assertEqual(("a", ""), split_filename("a/"))
24-
self.assertEqual(("/a", ""), split_filename("/a/"))
25-
26-
def test_split_filename_2c(self):
27-
self.assertEqual(("a", "b"), split_filename("a/b"))
28-
self.assertEqual(("/a", "b"), split_filename("/a/b"))
29-
self.assertEqual(("a/b", ""), split_filename("a/b/"))
30-
self.assertEqual(("/a/b", ""), split_filename("/a/b/"))
31-
32-
def test_split_filename_3c(self):
33-
self.assertEqual(("a/b", "c"), split_filename("a/b/c"))
34-
self.assertEqual(("/a/b", "c"), split_filename("/a/b/c"))
35-
self.assertEqual(("a/b/c", ""), split_filename("a/b/c/"))
36-
self.assertEqual(("/a/b/c", ""), split_filename("/a/b/c/"))
17+
split_parent("")
18+
19+
def test_split_parent_1c(self):
20+
self.assertEqual(("/", ""), split_parent("/"))
21+
self.assertEqual(("", "a"), split_parent("a"))
22+
self.assertEqual(("/", "a"), split_parent("/a"))
23+
self.assertEqual(("a", ""), split_parent("a/"))
24+
self.assertEqual(("/a", ""), split_parent("/a/"))
25+
26+
def test_split_parent_2c(self):
27+
self.assertEqual(("a", "b"), split_parent("a/b"))
28+
self.assertEqual(("/a", "b"), split_parent("/a/b"))
29+
self.assertEqual(("a/b", ""), split_parent("a/b/"))
30+
self.assertEqual(("/a/b", ""), split_parent("/a/b/"))
31+
32+
def test_split_parent_3c(self):
33+
self.assertEqual(("a/b", "c"), split_parent("a/b/c"))
34+
self.assertEqual(("/a/b", "c"), split_parent("/a/b/c"))
35+
self.assertEqual(("a/b/c", ""), split_parent("a/b/c/"))
36+
self.assertEqual(("/a/b/c", ""), split_parent("/a/b/c/"))
3737

3838

3939
class SplitComponentsTest(unittest.TestCase):

tests/fsutil/test_transaction.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def test_it_raises_on_locked_target(self):
141141
test_root.mkdir()
142142
rollback_dir = FileObj("memory://rollback")
143143
with Transaction(test_root, rollback_dir):
144-
with pytest.raises(OSError, match="Target is locked: memory:///test.lock"):
144+
with pytest.raises(OSError, match="Target is locked: memory://test.lock"):
145145
with Transaction(test_root, rollback_dir):
146146
pass
147147

@@ -239,3 +239,35 @@ def test_it_raises_on_illegal_callback_calls(self):
239239
):
240240
with Transaction(test_root, rollback_dir) as callback:
241241
callback("replace_ifle", "I/am/the/path", b"I/m/the/data")
242+
243+
def test_paths_for_uri(self):
244+
t = Transaction(FileObj("memory:///target.zarr"), FileObj("memory:///temp"))
245+
self.assertEqual(FileObj("memory:///target.zarr"), t.target_dir)
246+
self.assertEqual(FileObj("memory:///target.zarr.lock"), t.lock_file)
247+
self.assertTrue(t.rollback_dir.uri.startswith("memory:///temp/zappend-"))
248+
# Note, 2 slashes only!
249+
t = Transaction(FileObj("memory://target.zarr"), FileObj("memory://temp"))
250+
self.assertEqual(FileObj("memory://target.zarr"), t.target_dir)
251+
self.assertEqual(FileObj("memory://target.zarr.lock"), t.lock_file)
252+
self.assertTrue(t.rollback_dir.uri.startswith("memory://temp/zappend-"))
253+
254+
def test_paths_for_local(self):
255+
t = Transaction(FileObj("./target.zarr"), FileObj("temp"))
256+
self.assertEqual(FileObj("./target.zarr"), t.target_dir)
257+
self.assertEqual(FileObj("./target.zarr.lock"), t.lock_file)
258+
self.assertTrue(t.rollback_dir.uri.startswith("temp/zappend-"))
259+
260+
t = Transaction(FileObj("out/target.zarr"), FileObj("out/temp"))
261+
self.assertEqual(FileObj("out/target.zarr"), t.target_dir)
262+
self.assertEqual(FileObj("out/target.zarr.lock"), t.lock_file)
263+
self.assertTrue(t.rollback_dir.uri.startswith("out/temp/zappend-"))
264+
265+
t = Transaction(FileObj("/out/target.zarr"), FileObj("/out/temp"))
266+
self.assertEqual(FileObj("/out/target.zarr"), t.target_dir)
267+
self.assertEqual(FileObj("/out/target.zarr.lock"), t.lock_file)
268+
self.assertTrue(t.rollback_dir.uri.startswith("/out/temp/zappend-"))
269+
270+
t = Transaction(FileObj("target.zarr"), FileObj("temp"))
271+
self.assertEqual(FileObj("target.zarr"), t.target_dir)
272+
self.assertEqual(FileObj("target.zarr.lock"), t.lock_file)
273+
self.assertTrue(t.rollback_dir.uri.startswith("temp/zappend-"))

tests/test_api.py

+23-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Permissions are hereby granted under the terms of the MIT License:
33
# https://opensource.org/licenses/MIT.
44
import os
5+
import shutil
56
import unittest
67

78
import xarray as xr
@@ -20,7 +21,7 @@ def test_no_slices(self):
2021
zappend([], target_dir=target_dir)
2122
self.assertFalse(FileObj(target_dir).exists())
2223

23-
def test_some_slices(self):
24+
def test_some_slices_memory(self):
2425
target_dir = "memory://target.zarr"
2526
slices = [make_test_dataset(), make_test_dataset(), make_test_dataset()]
2627
zappend(slices, target_dir=target_dir)
@@ -29,16 +30,35 @@ def test_some_slices(self):
2930
self.assertEqual({"chl", "tsm"}, set(ds.data_vars))
3031
self.assertEqual({"time", "y", "x"}, set(ds.coords))
3132

33+
def test_some_slices_local(self):
34+
target_dir = "target.zarr"
35+
slices = [
36+
"slice-1.zarr",
37+
"slice-2.zarr",
38+
"slice-3.zarr",
39+
]
40+
for uri in slices:
41+
make_test_dataset(uri=uri)
42+
try:
43+
zappend(slices, target_dir=target_dir)
44+
ds = xr.open_zarr(target_dir)
45+
self.assertEqual({"time": 9, "y": 50, "x": 100}, ds.sizes)
46+
self.assertEqual({"chl", "tsm"}, set(ds.data_vars))
47+
self.assertEqual({"time", "y", "x"}, set(ds.coords))
48+
finally:
49+
shutil.rmtree(target_dir, ignore_errors=True)
50+
for slice_dir in slices:
51+
shutil.rmtree(slice_dir, ignore_errors=True)
52+
3253
def test_some_slices_with_profiling(self):
54+
target_dir = "memory://target.zarr"
3355
slices = [
3456
"memory://slice-1.zarr",
3557
"memory://slice-2.zarr",
3658
"memory://slice-3.zarr",
3759
]
3860
for uri in slices:
3961
make_test_dataset(uri=uri)
40-
41-
target_dir = "memory://target.zarr"
4262
try:
4363
zappend(
4464
slices,

zappend/fsutil/fileobj.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import fsspec
99

10-
from .path import split_filename
10+
from .path import split_parent
1111

1212

1313
# Note, we could make FileObj an ABC and then introduce concrete
@@ -106,7 +106,7 @@ def close(self):
106106
@property
107107
def filename(self) -> str:
108108
"""The filename part of the URI."""
109-
return split_filename(self.path)[1]
109+
return split_parent(self.path)[1]
110110

111111
@property
112112
def parent(self) -> "FileObj":
@@ -115,21 +115,21 @@ def parent(self) -> "FileObj":
115115
# If uri is a chained URL, use path of first component
116116
first_uri, rest = self.uri.split("::", maxsplit=1)
117117
protocol, path = fsspec.core.split_protocol(first_uri)
118-
parent_path, _ = split_filename(path)
118+
parent_path, _ = split_parent(path)
119119
if protocol:
120120
new_uri = f"{protocol}://{parent_path}::{rest}"
121121
else:
122122
new_uri = f"{parent_path}::{rest}"
123123
else:
124124
protocol, path = fsspec.core.split_protocol(self.uri)
125-
parent_path, _ = split_filename(path)
125+
parent_path, _ = split_parent(path)
126126
if protocol:
127127
new_uri = f"{protocol}://{parent_path}"
128128
else:
129129
new_uri = parent_path
130130

131131
if self._path is not None:
132-
new_path, _ = split_filename(self._path)
132+
new_path, _ = split_parent(self._path)
133133
else:
134134
# it is ok, we are still unresolved
135135
new_path = None
@@ -171,6 +171,10 @@ def for_path(self, rel_path: str) -> "FileObj":
171171
# If uri is a chained URL, add path to first component
172172
first_uri, rest = self.uri.split("::", maxsplit=1)
173173
new_uri = f"{first_uri}/{rel_path}::{rest}"
174+
elif not self.uri:
175+
new_uri = rel_path
176+
elif self.uri.endswith("/"):
177+
new_uri = self.uri + rel_path
174178
else:
175179
new_uri = f"{self.uri}/{rel_path}"
176180

zappend/fsutil/path.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
# https://opensource.org/licenses/MIT.
44

55

6-
def split_filename(path: str, sep: str = "/") -> tuple[str, str]:
6+
def split_parent(path: str, sep: str = "/") -> tuple[str, str]:
77
if not path:
88
raise ValueError("cannot get parent of empty path")
99
comps = path.rsplit(sep, maxsplit=1)
1010
if len(comps) == 1:
1111
return "", comps[0]
12-
return comps[0], comps[1]
12+
parent, rest = comps
13+
return parent or "/", rest
1314

1415

1516
def split_components(path: str, sep: str = "/") -> list[str]:

0 commit comments

Comments
 (0)