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 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
12 changes: 12 additions & 0 deletions src/zarr/storage/_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,5 +253,17 @@
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.parent, exist_ok=True)
if os.path.exists(dest_root):
raise FileExistsError(f"Destination root {dest_root} already exists.")
shutil.move(self.root, dest_root)
self.root = dest_root

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L260 - L266 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
43 changes: 39 additions & 4 deletions tests/test_store/test_local.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
from __future__ import annotations

from typing import TYPE_CHECKING
import pathlib
import re

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
from zarr.testing.utils import assert_bytes_equal

if TYPE_CHECKING:
import pathlib


class TestLocalStore(StoreTests[LocalStore, cpu.Buffer]):
store_cls = LocalStore
Expand Down Expand Up @@ -74,3 +74,38 @@ async def test_get_with_prototype_default(self, store: LocalStore) -> None:
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, 3])
@pytest.mark.parametrize(
"destination", ["destination", "foo/bar/destintion", pathlib.Path("foo/bar/destintion")]
)
async def test_move(
self, tmp_path: pathlib.Path, ndim: int, destination: pathlib.Path | str
) -> None:
origin = tmp_path / "origin"
if isinstance(destination, str):
destination = str(tmp_path / destination)
else:
destination = tmp_path / destination

print(type(destination))
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(destination)

assert store.root == pathlib.Path(destination)
assert pathlib.Path(destination).exists()
assert not origin.exists()
assert np.array_equal(array[...], data)

store2 = await LocalStore.open(root=origin)
with pytest.raises(
FileExistsError, match=re.escape(f"Destination root {destination} already exists")
):
await store2.move(destination)
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.core.group import Group
from zarr.storage import ZipStore
Expand Down Expand Up @@ -140,3 +141,17 @@ def test_externally_zipped_store(self, tmp_path: Path) -> None:
assert list(zipped.keys()) == list(root.keys())
assert isinstance(group := zipped["foo"], Group)
assert list(group.keys()) == list(group.keys())

async def test_move(self, tmp_path: Path) -> None:
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))