Skip to content

Implement Store.move #3021

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions changes/3021.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implemented ``move`` for ``LocalStore`` and ``ZipStore``. This allows users to move the store to a different root path.
16 changes: 16 additions & 0 deletions src/zarr/storage/_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,5 +253,21 @@
except (FileNotFoundError, NotADirectoryError):
pass

async def move(self, dest_root: Path | str) -> None:
"""
Move the store to another path. The old root directory is deleted.
"""
if isinstance(dest_root, str):
dest_root = Path(dest_root)
os.makedirs(dest_root, exist_ok=True)
for src_file in self.root.rglob("*"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use shutil.move to move the entire folder?
If implementing this ourselves, the newly created folders might need to copy the permissions and settings of the source folders. And, we could put the move operations on a thread pool.

if src_file.is_file():
relative_path = src_file.relative_to(self.root)
dest_file_path = dest_root / relative_path
dest_file_path.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(src_file), str(dest_file_path))
shutil.rmtree(self.root)
self.root = dest_root

Check warning on line 270 in src/zarr/storage/_local.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/storage/_local.py#L260-L270

Added lines #L260 - L270 were not covered by tests

async def getsize(self, key: str) -> int:
return os.path.getsize(self.root / key)
13 changes: 13 additions & 0 deletions src/zarr/storage/_zip.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
import shutil
import threading
import time
import zipfile
Expand Down Expand Up @@ -288,3 +289,15 @@
if k not in seen:
seen.add(k)
yield k

async def move(self, path: Path | str) -> None:
"""
Move the store to another path.
"""
if isinstance(path, str):
path = Path(path)
self.close()
os.makedirs(path.parent, exist_ok=True)
shutil.move(self.path, path)
self.path = path
await self._open()

Check warning on line 303 in src/zarr/storage/_zip.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/storage/_zip.py#L297-L303

Added lines #L297 - L303 were not covered by tests
22 changes: 22 additions & 0 deletions tests/test_store/test_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

from typing import TYPE_CHECKING

import numpy as np
import pytest

import zarr
from zarr import create_array
from zarr.core.buffer import Buffer, cpu
from zarr.storage import LocalStore
from zarr.testing.store import StoreTests
Expand Down Expand Up @@ -74,3 +76,23 @@ async def test_get_with_prototype_default(self, store: LocalStore):
await self.set(store, key, data_buf)
observed = await store.get(key, prototype=None)
assert_bytes_equal(observed, data_buf)

@pytest.mark.parametrize("ndim", [0, 1, 2, 3])
async def test_move(self, tmp_path: pathlib.Path, ndim):
origin = tmp_path / "origin"
destination = tmp_path / "destintion"

store = await LocalStore.open(root=origin)
shape = (4,) * ndim
chunks = (2,) * ndim
data = np.arange(4**ndim)
if ndim > 0:
data = data.reshape(*shape)
array = create_array(store, data=data, chunks=chunks or "auto")

await store.move(str(destination))

assert store.root == destination
assert destination.exists()
assert not origin.exists()
assert np.array_equal(array[...], data)
15 changes: 15 additions & 0 deletions tests/test_store/test_zip.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import pytest

import zarr
from zarr import create_array
from zarr.core.buffer import Buffer, cpu, default_buffer_prototype
from zarr.storage import ZipStore
from zarr.testing.store import StoreTests
Expand Down Expand Up @@ -135,3 +136,17 @@ def test_externally_zipped_store(self, tmp_path: Path) -> None:
zipped = zarr.open_group(ZipStore(zip_path, mode="r"), mode="r")
assert list(zipped.keys()) == list(root.keys())
assert list(zipped["foo"].keys()) == list(root["foo"].keys())

async def test_move(self, tmp_path: Path):
origin = tmp_path / "origin.zip"
destination = tmp_path / "some_folder" / "destination.zip"

store = await ZipStore.open(path=origin, mode="a")
array = create_array(store, data=np.arange(10))

await store.move(str(destination))

assert store.path == destination
assert destination.exists()
assert not origin.exists()
assert np.array_equal(array[...], np.arange(10))