Skip to content

Commit 9c6eb11

Browse files
committed
Add DictStore.to_b2d method for consistency
1 parent f494c63 commit 9c6eb11

5 files changed

Lines changed: 141 additions & 15 deletions

File tree

doc/reference/dict_store.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,11 @@ Quick example
8686
.. automethod:: __enter__
8787
.. automethod:: __exit__
8888

89+
Persistence
90+
-----------
91+
Use :meth:`DictStore.to_b2z` to pack a directory-backed store into a
92+
``.b2z`` archive, and :meth:`DictStore.to_b2d` to materialize a store as a
93+
``.b2d`` directory.
94+
8995
Public Members
9096
--------------

src/blosc2/ctable.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import pprint
2020
import re
2121
import shutil
22-
import zipfile
2322
from collections import namedtuple
2423
from collections.abc import Iterable, Mapping
2524
from dataclasses import MISSING, dataclass
@@ -2324,20 +2323,11 @@ def to_b2d(self, urlpath: str, *, overwrite: bool = False, compact: bool = False
23242323
and storage.open_mode() == "r"
23252324
)
23262325
if can_physical_unpack:
2327-
target_path = os.fspath(urlpath)
2328-
if os.path.exists(target_path):
2329-
if not overwrite:
2330-
raise FileExistsError(
2331-
f"'{target_path}' already exists. Use overwrite=True to overwrite."
2332-
)
2333-
if os.path.isdir(target_path):
2334-
shutil.rmtree(target_path)
2335-
else:
2336-
os.remove(target_path)
2337-
os.makedirs(target_path, exist_ok=True)
2338-
with zipfile.ZipFile(storage._root, "r") as zf:
2339-
zf.extractall(target_path)
2340-
return os.path.abspath(target_path)
2326+
store = blosc2.TreeStore(storage._root, mode="r")
2327+
try:
2328+
return store.to_b2d(urlpath, overwrite=overwrite)
2329+
finally:
2330+
store.close()
23412331

23422332
if self.base is not None:
23432333
materialized = self.copy(compact=True)

src/blosc2/dict_store.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,21 @@ def to_b2z(self, overwrite=False, filename=None) -> os.PathLike[Any] | str:
632632
-------
633633
filename : str
634634
The absolute path to the created b2z file.
635+
636+
Examples
637+
--------
638+
Pack a directory-backed store into a zip store::
639+
640+
with blosc2.DictStore("data.b2d", mode="w") as dstore:
641+
dstore["/values"] = np.arange(10)
642+
643+
with blosc2.DictStore("data.b2d", mode="r") as dstore:
644+
dstore.to_b2z(filename="data.b2z", overwrite=True)
645+
646+
``filename`` can also be passed positionally::
647+
648+
with blosc2.DictStore("data.b2d", mode="r") as dstore:
649+
dstore.to_b2z("copy.b2z", overwrite=True)
635650
"""
636651
if isinstance(overwrite, str | os.PathLike) and filename is None:
637652
filename = overwrite
@@ -641,6 +656,7 @@ def to_b2z(self, overwrite=False, filename=None) -> os.PathLike[Any] | str:
641656
raise ValueError("Cannot call to_b2z() on a .b2z DictStore opened in read mode.")
642657

643658
b2z_path = self.b2z_path if filename is None else filename
659+
b2z_path = os.fspath(b2z_path)
644660
if not b2z_path.endswith(".b2z"):
645661
raise ValueError("b2z_path must have a .b2z extension")
646662

@@ -669,6 +685,68 @@ def to_b2z(self, overwrite=False, filename=None) -> os.PathLike[Any] | str:
669685
zf.write(self.estore_path, arcname)
670686
return os.path.abspath(b2z_path)
671687

688+
def to_b2d(self, dirname=None, *, overwrite: bool = False) -> os.PathLike[Any] | str:
689+
"""
690+
Serialize store contents to a b2d directory.
691+
692+
Parameters
693+
----------
694+
dirname : str, optional
695+
If provided, use this directory instead of the default b2d path.
696+
overwrite : bool, optional
697+
If True, overwrite the existing b2d directory if it exists.
698+
Default is False.
699+
700+
Returns
701+
-------
702+
dirname : str
703+
The absolute path to the created b2d directory.
704+
705+
Examples
706+
--------
707+
Unpack a zip-backed store into a directory-backed store::
708+
709+
with blosc2.DictStore("data.b2z", mode="r") as dstore:
710+
dstore.to_b2d("data.b2d", overwrite=True)
711+
712+
with blosc2.DictStore("data.b2d", mode="r") as dstore:
713+
values = dstore["/values"][:]
714+
715+
Copy an existing directory-backed store to another ``.b2d`` directory::
716+
717+
with blosc2.DictStore("data.b2d", mode="r") as dstore:
718+
dstore.to_b2d("backup.b2d", overwrite=True)
719+
"""
720+
b2d_path = self.localpath if dirname is None and not self.is_zip_store else dirname
721+
if b2d_path is None:
722+
b2d_path = (
723+
self.b2z_path[:-4] + ".b2d" if self.b2z_path.endswith(".b2z") else self.b2z_path + ".b2d"
724+
)
725+
b2d_path = os.fspath(b2d_path)
726+
if not b2d_path.endswith(".b2d"):
727+
raise ValueError("b2d_path must have a .b2d extension")
728+
729+
target_path = os.path.abspath(b2d_path)
730+
source_path = os.path.abspath(self.working_dir)
731+
if not self.is_zip_store and target_path == source_path:
732+
return target_path
733+
734+
if os.path.exists(target_path):
735+
if not overwrite:
736+
raise FileExistsError(f"'{target_path}' already exists. Use overwrite=True to overwrite.")
737+
if os.path.isdir(target_path):
738+
shutil.rmtree(target_path)
739+
else:
740+
os.remove(target_path)
741+
742+
if self.is_zip_store and self.mode == "r":
743+
os.makedirs(target_path, exist_ok=True)
744+
with zipfile.ZipFile(self.b2z_path, "r") as zf:
745+
zf.extractall(target_path)
746+
else:
747+
shutil.copytree(self.working_dir, target_path)
748+
return target_path
749+
672750
def _get_zip_offsets(self) -> dict[str, dict[str, int]]:
673751
"""Get offset and length of all files in the zip archive."""
674752
self.offsets = {} # Reset offsets

tests/test_dict_store.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,41 @@ def test_to_b2z_accepts_positional_filename():
179179
os.remove(b2z_path)
180180

181181

182+
def test_to_b2d_from_readonly_b2z(tmp_path):
183+
b2z_path = tmp_path / "test_to_b2d_src.b2z"
184+
b2d_path = tmp_path / "test_to_b2d_dst.b2d"
185+
186+
with DictStore(str(b2z_path), mode="w") as dstore:
187+
dstore["/nodeA"] = np.arange(5)
188+
dstore["/nodeB"] = np.arange(6)
189+
190+
with DictStore(str(b2z_path), mode="r") as dstore:
191+
unpacked = dstore.to_b2d(str(b2d_path))
192+
assert unpacked == os.path.abspath(b2d_path)
193+
194+
with DictStore(str(b2d_path), mode="r") as dstore:
195+
assert np.all(dstore["/nodeA"][:] == np.arange(5))
196+
assert np.all(dstore["/nodeB"][:] == np.arange(6))
197+
198+
199+
def test_to_b2d_overwrite_existing_raises(tmp_path):
200+
b2z_path = tmp_path / "test_to_b2d_existing.b2z"
201+
b2d_path = tmp_path / "test_to_b2d_existing.b2d"
202+
203+
with DictStore(str(b2z_path), mode="w") as dstore:
204+
dstore["/nodeA"] = np.arange(5)
205+
b2d_path.mkdir()
206+
207+
with DictStore(str(b2z_path), mode="r") as dstore, pytest.raises(FileExistsError):
208+
dstore.to_b2d(str(b2d_path))
209+
210+
with DictStore(str(b2z_path), mode="r") as dstore:
211+
dstore.to_b2d(str(b2d_path), overwrite=True)
212+
213+
with DictStore(str(b2d_path), mode="r") as dstore:
214+
assert np.all(dstore["/nodeA"][:] == np.arange(5))
215+
216+
182217
def test_to_b2z_from_readonly_b2z_raises():
183218
b2z_path = "test_to_b2z_readonly_zip.b2z"
184219
out_path = "test_to_b2z_readonly_zip_out.b2z"

tests/test_tree_store.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,23 @@ def test_open_context_manager(populated_tree_store):
10571057
assert np.array_equal(tstore["/child0/data"][:], np.array([1, 2, 3]))
10581058

10591059

1060+
def test_to_b2d_from_readonly_b2z(tmp_path):
1061+
b2z_path = tmp_path / "test_tstore_to_b2d.b2z"
1062+
b2d_path = tmp_path / "test_tstore_to_b2d.b2d"
1063+
1064+
with TreeStore(str(b2z_path), mode="w") as tstore:
1065+
tstore["/group/node"] = np.arange(6)
1066+
tstore.vlmeta["description"] = "tree metadata"
1067+
1068+
with TreeStore(str(b2z_path), mode="r") as tstore:
1069+
unpacked = tstore.to_b2d(str(b2d_path))
1070+
assert unpacked == os.path.abspath(b2d_path)
1071+
1072+
with TreeStore(str(b2d_path), mode="r") as tstore:
1073+
assert np.array_equal(tstore["/group/node"][:], np.arange(6))
1074+
assert tstore.vlmeta["description"] == "tree metadata"
1075+
1076+
10601077
def test_extensionless_tree_store_defaults_to_directory(tmp_path):
10611078
path = tmp_path / "test_tstore_extless"
10621079

0 commit comments

Comments
 (0)