Skip to content

Commit 18c4a9e

Browse files
authored
Implement Store.move (#3021)
* move for LocalStore * move for ZipStore * remove redundant check * open and close zipstore * fix zipstore.move * fix localstore.move for ndim>1 * format * remove abstract Store .move * document changes * shutil.move entire folder * fix test for windows
1 parent 80a09d7 commit 18c4a9e

File tree

5 files changed

+80
-4
lines changed

5 files changed

+80
-4
lines changed

changes/3021.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implemented ``move`` for ``LocalStore`` and ``ZipStore``. This allows users to move the store to a different root path.

src/zarr/storage/_local.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,5 +253,17 @@ async def list_dir(self, prefix: str) -> AsyncIterator[str]:
253253
except (FileNotFoundError, NotADirectoryError):
254254
pass
255255

256+
async def move(self, dest_root: Path | str) -> None:
257+
"""
258+
Move the store to another path. The old root directory is deleted.
259+
"""
260+
if isinstance(dest_root, str):
261+
dest_root = Path(dest_root)
262+
os.makedirs(dest_root.parent, exist_ok=True)
263+
if os.path.exists(dest_root):
264+
raise FileExistsError(f"Destination root {dest_root} already exists.")
265+
shutil.move(self.root, dest_root)
266+
self.root = dest_root
267+
256268
async def getsize(self, key: str) -> int:
257269
return os.path.getsize(self.root / key)

src/zarr/storage/_zip.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import os
4+
import shutil
45
import threading
56
import time
67
import zipfile
@@ -288,3 +289,15 @@ async def list_dir(self, prefix: str) -> AsyncIterator[str]:
288289
if k not in seen:
289290
seen.add(k)
290291
yield k
292+
293+
async def move(self, path: Path | str) -> None:
294+
"""
295+
Move the store to another path.
296+
"""
297+
if isinstance(path, str):
298+
path = Path(path)
299+
self.close()
300+
os.makedirs(path.parent, exist_ok=True)
301+
shutil.move(self.path, path)
302+
self.path = path
303+
await self._open()

tests/test_store/test_local.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING
3+
import pathlib
4+
import re
45

6+
import numpy as np
57
import pytest
68

79
import zarr
10+
from zarr import create_array
811
from zarr.core.buffer import Buffer, cpu
912
from zarr.storage import LocalStore
1013
from zarr.testing.store import StoreTests
1114
from zarr.testing.utils import assert_bytes_equal
1215

13-
if TYPE_CHECKING:
14-
import pathlib
15-
1616

1717
class TestLocalStore(StoreTests[LocalStore, cpu.Buffer]):
1818
store_cls = LocalStore
@@ -74,3 +74,38 @@ async def test_get_with_prototype_default(self, store: LocalStore) -> None:
7474
await self.set(store, key, data_buf)
7575
observed = await store.get(key, prototype=None)
7676
assert_bytes_equal(observed, data_buf)
77+
78+
@pytest.mark.parametrize("ndim", [0, 1, 3])
79+
@pytest.mark.parametrize(
80+
"destination", ["destination", "foo/bar/destintion", pathlib.Path("foo/bar/destintion")]
81+
)
82+
async def test_move(
83+
self, tmp_path: pathlib.Path, ndim: int, destination: pathlib.Path | str
84+
) -> None:
85+
origin = tmp_path / "origin"
86+
if isinstance(destination, str):
87+
destination = str(tmp_path / destination)
88+
else:
89+
destination = tmp_path / destination
90+
91+
print(type(destination))
92+
store = await LocalStore.open(root=origin)
93+
shape = (4,) * ndim
94+
chunks = (2,) * ndim
95+
data = np.arange(4**ndim)
96+
if ndim > 0:
97+
data = data.reshape(*shape)
98+
array = create_array(store, data=data, chunks=chunks or "auto")
99+
100+
await store.move(destination)
101+
102+
assert store.root == pathlib.Path(destination)
103+
assert pathlib.Path(destination).exists()
104+
assert not origin.exists()
105+
assert np.array_equal(array[...], data)
106+
107+
store2 = await LocalStore.open(root=origin)
108+
with pytest.raises(
109+
FileExistsError, match=re.escape(f"Destination root {destination} already exists")
110+
):
111+
await store2.move(destination)

tests/test_store/test_zip.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import pytest
1111

1212
import zarr
13+
from zarr import create_array
1314
from zarr.core.buffer import Buffer, cpu, default_buffer_prototype
1415
from zarr.core.group import Group
1516
from zarr.storage import ZipStore
@@ -140,3 +141,17 @@ def test_externally_zipped_store(self, tmp_path: Path) -> None:
140141
assert list(zipped.keys()) == list(root.keys())
141142
assert isinstance(group := zipped["foo"], Group)
142143
assert list(group.keys()) == list(group.keys())
144+
145+
async def test_move(self, tmp_path: Path) -> None:
146+
origin = tmp_path / "origin.zip"
147+
destination = tmp_path / "some_folder" / "destination.zip"
148+
149+
store = await ZipStore.open(path=origin, mode="a")
150+
array = create_array(store, data=np.arange(10))
151+
152+
await store.move(str(destination))
153+
154+
assert store.path == destination
155+
assert destination.exists()
156+
assert not origin.exists()
157+
assert np.array_equal(array[...], np.arange(10))

0 commit comments

Comments
 (0)