diff --git a/brukerapi/paths.py b/brukerapi/paths.py index 1ffb23a..19fa031 100644 --- a/brukerapi/paths.py +++ b/brukerapi/paths.py @@ -71,9 +71,14 @@ def read_array(path, dtype, shape, order="F"): A filesystem path is memory-mapped as before. An archive member cannot be (a memory map cannot address a compressed member), so it degrades to a full read. + + The map is opened read-only. numpy's default mode is "r+", which asks the + operating system for write access and so fails with EACCES on a read-only file + or mount -- the usual way a shared scanner archive is exposed. Nothing here + writes: the array is copied out on the same line. """ if isinstance(path, (str, os.PathLike)): - return np.array(np.memmap(path, dtype=dtype, shape=shape, order=order)[:]) + return np.array(np.memmap(path, dtype=dtype, mode="r", shape=shape, order=order)[:]) with path.open("rb") as binary: buffer = binary.read() return np.frombuffer(buffer, dtype=dtype).reshape(shape, order=order) diff --git a/test/test_paths.py b/test/test_paths.py index febe765..4926c60 100644 --- a/test/test_paths.py +++ b/test/test_paths.py @@ -82,6 +82,28 @@ def test_dataset_reads_from_archive_identically(study_dir, study_zip): assert from_zip.shape_final == from_dir.shape_final +def test_dataset_reads_from_a_read_only_source(study_dir): + """A dataset whose files are not writable must still load. + + Scanner archives are normally exposed read-only, and nothing here writes: the + array is copied out of the map immediately. numpy's default memmap mode is + "r+", which asks the operating system for write access and fails with EACCES + on such a source, so the mode has to be given explicitly. + """ + for path in sorted(study_dir.rglob("*")): + if path.is_file(): + path.chmod(0o444) + + try: + dataset = Dataset(study_dir / "1" / "pdata" / "1" / "2dseq", scale=False) + assert np.array_equal(dataset.data, DATA) + finally: + # restore, so pytest can clean the temporary directory up + for path in sorted(study_dir.rglob("*")): + if path.is_file(): + path.chmod(0o644) + + def test_parameters_resolve_through_relative_paths(study_dir, study_zip): """``../../acqp`` resolves inside an archive, where ``..`` is not collapsed.""" dataset = Dataset(study_zip / "1" / "pdata" / "1" / "2dseq", scale=False,