Skip to content

Commit 1e12976

Browse files
committed
Normalize key with leading '/' added if missing.
1 parent bd43e22 commit 1e12976

File tree

2 files changed

+67
-15
lines changed

2 files changed

+67
-15
lines changed

src/blosc2/tree_store.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ class TreeStore(DictStore):
8484
8585
Extends DictStore with strict hierarchical key validation and tree traversal
8686
capabilities. Keys must follow a hierarchical structure using '/' as separator
87-
and always start with '/'.
87+
and always start with '/'. If user passes a key that doesn't start with '/',
88+
it will be automatically added.
8889
8990
Parameters
9091
----------
@@ -195,13 +196,18 @@ def _translate_key_from_full(self, full_key: str) -> str | None:
195196
# Key is not within this subtree
196197
return None
197198

198-
def _validate_key(self, key: str) -> None:
199-
"""Validate hierarchical key structure.
199+
def _validate_key(self, key: str) -> str:
200+
"""Validate and normalize hierarchical key structure.
200201
201202
Parameters
202203
----------
203204
key : str
204-
The key to validate.
205+
The key to validate and normalize.
206+
207+
Returns
208+
-------
209+
normalized_key : str
210+
The normalized key with leading '/' added if missing.
205211
206212
Raises
207213
------
@@ -211,8 +217,9 @@ def _validate_key(self, key: str) -> None:
211217
if not isinstance(key, str):
212218
raise ValueError(f"Key must be a string, got {type(key)}")
213219

220+
# Auto-add leading '/' if missing
214221
if not key.startswith("/"):
215-
raise ValueError(f"Key must start with '/', got: {key}")
222+
key = "/" + key
216223

217224
if key != "/" and key.endswith("/"):
218225
raise ValueError(f"Key cannot end with '/' (except for root), got: {key}")
@@ -226,13 +233,15 @@ def _validate_key(self, key: str) -> None:
226233
if char in key:
227234
raise ValueError(f"Key cannot contain invalid character {char!r}, got: {key}")
228235

236+
return key
237+
229238
def __setitem__(self, key: str, value: np.ndarray | NDArray | C2Array | SChunk) -> None:
230239
"""Add a node with hierarchical key validation.
231240
232241
Parameters
233242
----------
234243
key : str
235-
Hierarchical node key (must start with '/' and use '/' as separator).
244+
Hierarchical node key.
236245
value : np.ndarray or blosc2.NDArray or blosc2.C2Array or blosc2.SChunk
237246
to store.
238247
@@ -243,7 +252,7 @@ def __setitem__(self, key: str, value: np.ndarray | NDArray | C2Array | SChunk)
243252
assign to a structural path that already has children, or if trying
244253
to add a child to a path that already contains data.
245254
"""
246-
self._validate_key(key)
255+
key = self._validate_key(key)
247256

248257
# Check if this key already has children (is a structural subtree)
249258
children = self.get_children(key)
@@ -293,7 +302,7 @@ def __getitem__(self, key: str) -> "NDArray | C2Array | SChunk | TreeStore":
293302
ValueError
294303
If key doesn't follow hierarchical structure rules.
295304
"""
296-
self._validate_key(key)
305+
key = self._validate_key(key)
297306
if self._is_vlmeta_key(key):
298307
raise KeyError(f"Key '{key}' not found; vlmeta keys are not directly accessible.")
299308

@@ -334,7 +343,7 @@ def __delitem__(self, key: str) -> None:
334343
ValueError
335344
If key doesn't follow hierarchical structure rules.
336345
"""
337-
self._validate_key(key)
346+
key = self._validate_key(key)
338347

339348
if self._is_vlmeta_key(key):
340349
raise KeyError(f"Key '{key}' not found; vlmeta keys are not directly accessible.")
@@ -379,7 +388,7 @@ def __contains__(self, key: str) -> bool:
379388
True if key exists, False otherwise.
380389
"""
381390
try:
382-
self._validate_key(key)
391+
key = self._validate_key(key)
383392
if self._is_vlmeta_key(key):
384393
return False
385394
full_key = self._translate_key_to_full(key)
@@ -436,7 +445,7 @@ def get_children(self, path: str) -> list[str]:
436445
children : list[str]
437446
List of direct child paths.
438447
"""
439-
self._validate_key(path)
448+
path = self._validate_key(path)
440449

441450
if path == "/":
442451
prefix = "/"
@@ -475,7 +484,7 @@ def get_descendants(self, path: str) -> list[str]:
475484
descendants : list[str]
476485
List of all descendant paths.
477486
"""
478-
self._validate_key(path)
487+
path = self._validate_key(path)
479488

480489
if path == "/":
481490
prefix = "/"
@@ -523,7 +532,7 @@ def walk(self, path: str = "/", topdown: bool = True) -> Iterator[tuple[str, lis
523532
>>> for path, children, nodes in tstore.walk("/child0", topdown=True):
524533
... print(f"Path: {path}, Children: {children}, Nodes: {nodes}")
525534
"""
526-
self._validate_key(path)
535+
path = self._validate_key(path)
527536

528537
# Get all direct children of this path
529538
direct_children = self.get_children(path)
@@ -583,7 +592,8 @@ def get_subtree(self, path: str) -> "TreeStore":
583592
Parameters
584593
----------
585594
path : str
586-
The path that will become the root of the subtree view (relative to current subtree).
595+
The path that will become the root of the subtree view (relative to current subtree,
596+
will be normalized to start with '/' if missing).
587597
588598
Returns
589599
-------
@@ -604,7 +614,7 @@ def get_subtree(self, path: str) -> "TreeStore":
604614
-----
605615
This is equivalent to `tstore[path]` when path is a structural path.
606616
"""
607-
self._validate_key(path)
617+
path = self._validate_key(path)
608618
full_path = self._translate_key_to_full(path)
609619

610620
# Create a new TreeStore instance that shares the same underlying storage

tests/test_tree_store.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,3 +879,45 @@ def test_vlmeta_subtree_read_write():
879879

880880
# Cleanup
881881
os.remove("test_vlmeta_subtree_rw.b2z")
882+
883+
884+
def test_key_normalization():
885+
"""Test that keys without leading '/' are automatically normalized."""
886+
with TreeStore("test_key_normalization.b2z", mode="w") as tstore:
887+
# Test assignment without leading '/'
888+
tstore["data1"] = np.array([1, 2, 3])
889+
tstore["group/data2"] = np.array([4, 5, 6])
890+
tstore["group/subgroup/data3"] = np.array([7, 8, 9])
891+
892+
# Keys should be normalized internally
893+
assert "/data1" in tstore
894+
assert "/group/data2" in tstore
895+
assert "/group/subgroup/data3" in tstore
896+
897+
# Access with and without leading '/' should work
898+
assert np.array_equal(tstore["data1"][:], np.array([1, 2, 3]))
899+
assert np.array_equal(tstore["/data1"][:], np.array([1, 2, 3]))
900+
assert np.array_equal(tstore["group/data2"][:], np.array([4, 5, 6]))
901+
assert np.array_equal(tstore["/group/data2"][:], np.array([4, 5, 6]))
902+
903+
# Structural access should also work
904+
group_subtree = tstore["group"]
905+
assert isinstance(group_subtree, TreeStore)
906+
assert "/data2" in group_subtree
907+
assert "/subgroup/data3" in group_subtree
908+
909+
# Test other methods work with non-leading '/' keys
910+
children = tstore.get_children("group")
911+
assert "/group/subgroup" in children
912+
913+
descendants = tstore.get_descendants("group")
914+
assert "/group/data2" in descendants
915+
assert "/group/subgroup/data3" in descendants
916+
917+
# Test contains with both formats
918+
assert "data1" in tstore
919+
assert "/data1" in tstore
920+
assert "group/data2" in tstore
921+
assert "/group/data2" in tstore
922+
923+
os.remove("test_key_normalization.b2z")

0 commit comments

Comments
 (0)