44from pathlib import Path
55from typing import Any , Iterable , Optional , Sequence
66import shutil
7+ import warnings
78
89import h5py
910
1516import numpy as np
1617
1718
19+ ROOT_ENCODING_TYPE = "anndata"
20+ ROOT_ENCODING_VERSION = "0.1.0"
21+
22+
1823@dataclass
1924class Store :
2025 backend : str
@@ -96,19 +101,63 @@ def open_store(path: Path, mode: str) -> Store:
96101 if backend == "zarr" :
97102 _require_zarr ()
98103 root = zarr .open_group (str (path ), mode = mode )
104+ if _is_writable_mode (mode ):
105+ ensure_anndata_root_attrs (root )
106+ else :
107+ warn_if_missing_anndata_root_attrs (root , path = path )
99108 return Store (backend = "zarr" , root = root , path = path )
100109 root = h5py .File (path , mode )
110+ if _is_writable_mode (mode ):
111+ ensure_anndata_root_attrs (root )
112+ else :
113+ warn_if_missing_anndata_root_attrs (root , path = path )
101114 return Store (backend = "hdf5" , root = root , path = path )
102115
103116
117+ def _decode_attr (value : Any ) -> Any :
118+ if isinstance (value , bytes ):
119+ return value .decode ("utf-8" )
120+ return value
121+
122+
123+ def _is_writable_mode (mode : str ) -> bool :
124+ return any (flag in mode for flag in ("w" , "a" , "+" , "x" ))
125+
126+
127+ def has_valid_anndata_root_attrs (root : Any ) -> bool :
128+ enc_type = _decode_attr (root .attrs .get ("encoding-type" , None ))
129+ enc_ver = _decode_attr (root .attrs .get ("encoding-version" , None ))
130+ return enc_type == ROOT_ENCODING_TYPE and enc_ver == ROOT_ENCODING_VERSION
131+
132+
133+ def ensure_anndata_root_attrs (root : Any ) -> None :
134+ root .attrs ["encoding-type" ] = ROOT_ENCODING_TYPE
135+ root .attrs ["encoding-version" ] = ROOT_ENCODING_VERSION
136+
137+
138+ def warn_if_missing_anndata_root_attrs (root : Any , * , path : Path ) -> None :
139+ if has_valid_anndata_root_attrs (root ):
140+ return
141+
142+ enc_type = _decode_attr (root .attrs .get ("encoding-type" , None ))
143+ enc_ver = _decode_attr (root .attrs .get ("encoding-version" , None ))
144+ warnings .warn (
145+ (
146+ f"Store '{ path } ' root has missing or invalid AnnData attrs "
147+ f"(encoding-type={ ROOT_ENCODING_TYPE !r} , encoding-version={ ROOT_ENCODING_VERSION !r} ). "
148+ f"Found encoding-type={ enc_type !r} , encoding-version={ enc_ver !r} ."
149+ ),
150+ UserWarning ,
151+ stacklevel = 2 ,
152+ )
153+
154+
104155def _normalize_attr_value (value : Any , target_backend : str ) -> Any :
105156 if target_backend == "zarr" :
106157 if isinstance (value , bytes ):
107158 return value .decode ("utf-8" )
108159 if isinstance (value , (list , tuple )):
109- return [
110- v .decode ("utf-8" ) if isinstance (v , bytes ) else v for v in value
111- ]
160+ return [v .decode ("utf-8" ) if isinstance (v , bytes ) else v for v in value ]
112161 if isinstance (value , np .ndarray ):
113162 if value .dtype .kind in ("S" , "O" ):
114163 return [
@@ -187,7 +236,9 @@ def create_dataset(
187236 if zarr_format == 3 :
188237 kwargs = dict (kwargs )
189238 kwargs .pop ("compressor" , None )
190- elif zarr_format == 2 and "compressors" in kwargs and "compressor" not in kwargs :
239+ elif (
240+ zarr_format == 2 and "compressors" in kwargs and "compressor" not in kwargs
241+ ):
191242 kwargs = dict (kwargs )
192243 compressors = kwargs .pop ("compressors" )
193244 if isinstance (compressors , (list , tuple )) and len (compressors ) == 1 :
@@ -234,8 +285,12 @@ def copy_dataset(src: Any, dst_group: Any, name: str) -> Any:
234285 return ds
235286
236287
237- def copy_tree (src_obj : Any , dst_group : Any , name : str , * , exclude : Iterable [str ] = ()) -> Any :
238- if is_hdf5_group (dst_group ) and (is_hdf5_group (src_obj ) or is_hdf5_dataset (src_obj )):
288+ def copy_tree (
289+ src_obj : Any , dst_group : Any , name : str , * , exclude : Iterable [str ] = ()
290+ ) -> Any :
291+ if is_hdf5_group (dst_group ) and (
292+ is_hdf5_group (src_obj ) or is_hdf5_dataset (src_obj )
293+ ):
239294 if not exclude :
240295 dst_group .copy (src_obj , dst_group , name )
241296 return dst_group [name ]
@@ -256,6 +311,9 @@ def copy_tree(src_obj: Any, dst_group: Any, name: str, *, exclude: Iterable[str]
256311
257312
258313def copy_store_contents (src_root : Any , dst_root : Any ) -> None :
314+ target_backend = "zarr" if is_zarr_group (dst_root ) else "hdf5"
315+ copy_attrs (src_root .attrs , dst_root .attrs , target_backend = target_backend )
316+ ensure_anndata_root_attrs (dst_root )
259317 for key in src_root .keys ():
260318 copy_tree (src_root [key ], dst_root , key )
261319
0 commit comments