Skip to content

Commit 5fc16b7

Browse files
committed
Relax DictStore and TreeStore path suffix requirements
1 parent 457b0ff commit 5fc16b7

6 files changed

Lines changed: 60 additions & 21 deletions

File tree

src/blosc2/ctable_storage.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -157,17 +157,11 @@ class FileTableStorage(TableStorage):
157157
def __init__(self, urlpath: str, mode: str) -> None:
158158
if mode not in ("r", "a", "w"):
159159
raise ValueError(f"mode must be 'r', 'a', or 'w'; got {mode!r}")
160-
self._root = self._normalize_root(urlpath)
160+
self._root = urlpath
161161
self._mode = mode
162162
self._meta: blosc2.SChunk | None = None
163163
self._store: blosc2.TreeStore | None = None
164164

165-
@staticmethod
166-
def _normalize_root(urlpath: str) -> str:
167-
if urlpath.endswith((".b2d", ".b2z")):
168-
return urlpath
169-
return f"{urlpath}.b2d"
170-
171165
# ------------------------------------------------------------------
172166
# Key helpers
173167
# ------------------------------------------------------------------

src/blosc2/dict_store.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class DictStore:
2929
"""
3030
Directory-based storage for compressed data using Blosc2.
3131
32-
Manages arrays in a directory (.b2d) or zip (.b2z) format.
32+
Manages arrays in a directory or zip-file backed format.
3333
3434
Supports the following types:
3535
@@ -46,10 +46,11 @@ class DictStore:
4646
Parameters
4747
----------
4848
localpath : str
49-
Local path for the directory (".b2d") or file (".b2z"); other extensions
50-
are not supported. If a directory is specified, it will be treated as
51-
a Blosc2 directory format (B2DIR). If a file is specified, it
52-
will be treated as a Blosc2 zip format (B2ZIP).
49+
Local path for the directory or zip file. Paths ending in ``.b2d`` and
50+
``.b2z`` remain the recommended conventions. If the path already exists,
51+
directories are treated as Blosc2 directory format (B2DIR) and files as
52+
Blosc2 zip format (B2ZIP). For new extensionless paths, directory-backed
53+
storage is used by default.
5354
mode : str, optional
5455
File mode ('r', 'w', 'a'). Default is 'a'.
5556
mmap_mode : str or None, optional
@@ -117,8 +118,6 @@ def __init__(
117118
See :class:`DictStore` for full documentation of parameters.
118119
"""
119120
self.localpath = localpath if isinstance(localpath, str | bytes) else str(localpath)
120-
if not self.localpath.endswith((".b2z", ".b2d")):
121-
raise ValueError(f"localpath must have a .b2z or .b2d extension; you passed: {self.localpath}")
122121
if mode not in ("r", "w", "a"):
123122
raise ValueError("For DictStore containers, mode must be 'r', 'w', or 'a'")
124123
if mmap_mode not in (None, "r"):
@@ -152,7 +151,16 @@ def __init__(
152151

153152
def _setup_paths_and_dirs(self, tmpdir: str | None):
154153
"""Set up working directories and paths."""
155-
self.is_zip_store = self.localpath.endswith(".b2z")
154+
localpath_exists = os.path.exists(self.localpath)
155+
if localpath_exists:
156+
self.is_zip_store = os.path.isfile(self.localpath)
157+
elif self.localpath.endswith(".b2z"):
158+
self.is_zip_store = True
159+
elif self.localpath.endswith(".b2d"):
160+
self.is_zip_store = False
161+
else:
162+
# Default extensionless new stores to directory-backed layout.
163+
self.is_zip_store = False
156164
if self.is_zip_store:
157165
if tmpdir is None:
158166
self._temp_dir_obj = tempfile.TemporaryDirectory()
@@ -161,11 +169,14 @@ def _setup_paths_and_dirs(self, tmpdir: str | None):
161169
self.working_dir = tmpdir
162170
os.makedirs(tmpdir, exist_ok=True)
163171
self.b2z_path = self.localpath
164-
else: # .b2d
172+
else:
165173
self.working_dir = self.localpath
166174
if self.mode in ("w", "a"):
167175
os.makedirs(self.working_dir, exist_ok=True)
168-
self.b2z_path = self.localpath[:-4] + ".b2z"
176+
if self.localpath.endswith(".b2d"):
177+
self.b2z_path = self.localpath[:-4] + ".b2z"
178+
else:
179+
self.b2z_path = self.localpath + ".b2z"
169180

170181
self.estore_path = os.path.join(self.working_dir, "embed.b2e")
171182

src/blosc2/schunk.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,11 +1529,11 @@ def _open_meta(path, off=0):
15291529

15301530
if urlpath.endswith(".b2e") and offset == 0:
15311531
return _open_meta(urlpath)
1532-
if urlpath.endswith(".b2d") and os.path.isdir(urlpath):
1532+
if os.path.isdir(urlpath):
15331533
embed_path = os.path.join(urlpath, "embed.b2e")
15341534
if os.path.exists(embed_path):
15351535
return _open_meta(embed_path)
1536-
if urlpath.endswith(".b2z") and os.path.isfile(urlpath):
1536+
if os.path.isfile(urlpath) and not urlpath.endswith(".b2e"):
15371537
try:
15381538
with open(urlpath, "rb") as f, zipfile.ZipFile(f) as zf:
15391539
for info in zf.infolist():

tests/ctable/test_schema_mutations.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,11 @@ def test_blosc2_open_raw_treestore_for_unknown_manifest_kind():
145145
assert np.array_equal(opened["/payload"][:], np.arange(3))
146146

147147

148-
def test_extensionless_ctable_path_resolves_to_b2d_store():
148+
def test_extensionless_ctable_path_uses_extensionless_store():
149149
path = os.path.join(TABLE_ROOT, "alias_ctable")
150150
t = CTable(Row, urlpath=path, mode="w", new_data=DATA10)
151151
t.close()
152-
assert os.path.exists(path + ".b2d")
152+
assert os.path.isdir(path)
153153
opened = blosc2.open(path, mode="r")
154154
assert isinstance(opened, CTable)
155155
np.testing.assert_array_equal(opened["id"].to_numpy(), np.arange(10))

tests/test_dict_store.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,23 @@ def test_to_b2z_and_reopen(populated_dict_store):
114114
assert np.all(dstore_read["/nodeB"][:] == np.arange(6))
115115

116116

117+
def test_extensionless_dict_store_defaults_to_directory(tmp_path):
118+
path = tmp_path / "test_dstore_extless"
119+
120+
with DictStore(str(path), mode="w") as dstore:
121+
dstore["/node1"] = np.arange(4)
122+
123+
assert path.is_dir()
124+
assert (path / "embed.b2e").exists()
125+
126+
with DictStore(str(path), mode="r") as dstore:
127+
assert np.array_equal(dstore["/node1"][:], np.arange(4))
128+
129+
opened = blosc2.open(str(path), mode="r")
130+
assert isinstance(opened, DictStore)
131+
assert np.array_equal(opened["/node1"][:], np.arange(4))
132+
133+
117134
def test_to_b2z_from_readonly_b2d():
118135
b2d_path = "test_to_b2z_from_readonly.b2d"
119136
b2z_path = "test_to_b2z_from_readonly.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_extensionless_tree_store_defaults_to_directory(tmp_path):
1061+
path = tmp_path / "test_tstore_extless"
1062+
1063+
with TreeStore(str(path), mode="w") as tstore:
1064+
tstore["/group/node"] = np.arange(6)
1065+
1066+
assert path.is_dir()
1067+
assert (path / "embed.b2e").exists()
1068+
1069+
with TreeStore(str(path), mode="r") as tstore:
1070+
assert np.array_equal(tstore["/group/node"][:], np.arange(6))
1071+
1072+
opened = blosc2.open(str(path), mode="r")
1073+
assert isinstance(opened, TreeStore)
1074+
assert np.array_equal(opened["/group/node"][:], np.arange(6))
1075+
1076+
10601077
@pytest.mark.parametrize("storage_type", ["b2d", "b2z"])
10611078
def test_mmap_mode_read_access(storage_type, tmp_path):
10621079
path = tmp_path / f"test_tstore_mmap.{storage_type}"

0 commit comments

Comments
 (0)