use std::path::Path;
use super::{NpzArchive, NpzError, read_npz_file, write_npz_file};
use crate::io::{from_npy_bytes, try_npy_bytes};
use crate::{Array, Element, Error, Result};
fn npz_err(e: NpzError) -> Error {
match e {
NpzError::Io(io) => Error::Io(io),
other => Error::Format(other.to_string()),
}
}
fn savez_impl<T: Element>(path: &Path, arrays: &[(&str, &Array<T>)], compress: bool) -> Result<()> {
let members: Vec<(String, Vec<u8>)> = arrays
.iter()
.map(|(name, a)| Ok(((*name).to_string(), try_npy_bytes(a)?)))
.collect::<Result<_>>()?;
write_npz_file(path, &members, compress).map_err(npz_err)
}
pub fn savez<T: Element>(path: impl AsRef<Path>, arrays: &[(&str, &Array<T>)]) -> Result<()> {
savez_impl(path.as_ref(), arrays, false)
}
pub fn savez_compressed<T: Element>(
path: impl AsRef<Path>,
arrays: &[(&str, &Array<T>)],
) -> Result<()> {
savez_impl(path.as_ref(), arrays, true)
}
pub fn load_npz<T: Element>(path: impl AsRef<Path>) -> Result<Vec<(String, Array<T>)>> {
read_npz_file(path)
.map_err(npz_err)?
.into_iter()
.map(|(name, blob)| Ok((name, from_npy_bytes::<T>(&blob)?)))
.collect()
}
#[derive(Debug)]
pub struct NpzFile {
archive: NpzArchive,
}
impl NpzFile {
pub fn open(path: impl AsRef<Path>) -> Result<NpzFile> {
Ok(NpzFile {
archive: NpzArchive::open(path).map_err(npz_err)?,
})
}
pub fn keys(&self) -> Vec<String> {
self.archive.keys()
}
pub fn get<T: Element>(&self, name: &str) -> Result<Array<T>> {
let blob = self.archive.read(name).map_err(npz_err)?;
from_npy_bytes::<T>(&blob)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::npz::testutil::{Python, TempDir};
#[test]
fn savez_load_roundtrip_f64() {
let dir = TempDir::new();
let a = Array::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
let b = Array::from_vec(vec![0.25; 5], &[5]);
for (compress, name) in [(false, "p.npz"), (true, "c.npz")] {
let path = dir.path(name);
if compress {
savez_compressed(&path, &[("a", &a), ("b", &b)]).unwrap();
} else {
savez(&path, &[("a", &a), ("b", &b)]).unwrap();
}
let loaded = load_npz::<f64>(&path).unwrap();
assert_eq!(loaded.len(), 2);
assert_eq!(loaded[0], ("a".to_string(), a.clone()));
assert_eq!(loaded[1], ("b".to_string(), b.clone()));
}
}
#[test]
fn load_npz_rejects_wrong_dtype() {
let dir = TempDir::new();
let path = dir.path("i.npz");
let a = Array::from_vec(vec![1_i64, 2, 3], &[3]);
savez(&path, &[("a", &a)]).unwrap();
assert!(matches!(load_npz::<f64>(&path), Err(Error::DType { .. })));
assert_eq!(load_npz::<i64>(&path).unwrap()[0].1, a);
}
#[test]
fn savez_rejects_duplicate_and_empty_names() {
let dir = TempDir::new();
let a = Array::from_vec(vec![1.0], &[1]);
assert!(savez(dir.path("d.npz"), &[("a", &a), ("a", &a)]).is_err());
assert!(savez(dir.path("e.npz"), &[("", &a)]).is_err());
}
#[test]
fn npzfile_is_lazy_and_reports_missing_names() {
let dir = TempDir::new();
let path = dir.path("f.npz");
let a = Array::from_vec(vec![1.0, 2.0], &[2]);
savez_compressed(&path, &[("a", &a)]).unwrap();
let f = NpzFile::open(&path).unwrap();
assert_eq!(f.keys(), ["a"]);
assert_eq!(f.get::<f64>("a").unwrap(), a);
assert!(matches!(f.get::<f64>("nope"), Err(Error::Format(_))));
assert!(NpzFile::open(dir.path("missing.npz")).is_err());
}
#[test]
fn numpy_reads_and_writes_the_same_files() {
let Some(py) = Python::get() else { return };
let dir = TempDir::new();
let a = Array::from_vec((0..12).map(f64::from).collect(), &[3, 4]);
let ours = dir.path("ours.npz");
savez_compressed(&ours, &[("a", &a)]).unwrap();
let theirs = dir.path("theirs.npz");
let script = "import sys, numpy as np
f = np.load(sys.argv[1])
assert f['a'].shape == (3, 4) and f['a'].sum() == 66.0
np.savez_compressed(sys.argv[2], z=np.arange(6, dtype=np.float64).reshape(2, 3))";
py.run(
script,
&[ours.to_str().unwrap(), theirs.to_str().unwrap()],
&[],
);
let loaded = load_npz::<f64>(&theirs).unwrap();
assert_eq!(loaded[0].0, "z");
assert_eq!(loaded[0].1.shape(), &[2, 3]);
assert_eq!(loaded[0].1[[1, 2]], 5.0);
}
}