Skip to content

Commit d88719a

Browse files
GreenK173JKBTeque5
authored
Add directory of the collection to SigMFCollection (#85)
* fix path handling for collections * unit test --------- Co-authored-by: JKB <j.kuben@era.aero> Co-authored-by: Teque5 <teque5@gmail.com>
1 parent 2274172 commit d88719a

File tree

2 files changed

+142
-41
lines changed

2 files changed

+142
-41
lines changed

sigmf/sigmffile.py

Lines changed: 66 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import warnings
1515
from collections import OrderedDict
1616
from os import path
17+
from pathlib import Path
1718

1819
import numpy as np
1920

@@ -176,7 +177,7 @@ def __init__(self, metadata=None, data_file=None, global_info=None, skip_checksu
176177
map_readonly: bool, default True
177178
Indicates whether assignments on the numpy.memmap are allowed.
178179
"""
179-
super(SigMFFile, self).__init__()
180+
super().__init__()
180181
self.data_file = None
181182
self.sample_count = 0
182183
self._memmap = None
@@ -731,22 +732,36 @@ class SigMFCollection(SigMFMetafile):
731732
]
732733
VALID_KEYS = {COLLECTION_KEY: VALID_COLLECTION_KEYS}
733734

734-
def __init__(self, metafiles=None, metadata=None, skip_checksums=False):
735-
"""Create a SigMF Collection object.
736-
737-
Parameters:
738-
739-
metafiles -- A list of SigMF metadata filenames objects comprising the Collection,
740-
there must be at least one file. If the files do not exist, this will
741-
raise a SigMFFileError.
742-
743-
metadata -- collection metadata to use, if not provided this will populate a
744-
minimal set of default metadata. The core:streams field will be
745-
regenerated automatically
735+
def __init__(self, metafiles: list = None, metadata: dict = None, base_path=None, skip_checksums: bool = False) -> None:
746736
"""
747-
super(SigMFCollection, self).__init__()
737+
Create a SigMF Collection object.
738+
739+
Parameters
740+
----------
741+
metafiles: list, optional
742+
A list of SigMF metadata filenames objects comprising the Collection.
743+
There should be at least one file.
744+
metadata: dict, optional
745+
Collection metadata to use, if not provided this will populate a minimal set of default metadata.
746+
The `core:streams` field will be regenerated automatically.
747+
base_path : str | bytes | PathLike, optional
748+
Base path of the collection recordings.
749+
skip_checksums : bool, optional
750+
If true will skip calculating checksum on datasets.
751+
752+
Raises
753+
------
754+
SigMFError
755+
If metadata files do not exist.
756+
"""
757+
super().__init__()
748758
self.skip_checksums = skip_checksums
749759

760+
if base_path is None:
761+
self.base_path = Path("")
762+
else:
763+
self.base_path = Path(base_path)
764+
750765
if metadata is None:
751766
self._metadata = {self.COLLECTION_KEY: {}}
752767
self._metadata[self.COLLECTION_KEY][self.STREAMS_KEY] = []
@@ -764,55 +779,64 @@ def __init__(self, metafiles=None, metadata=None, skip_checksums=False):
764779
if not self.skip_checksums:
765780
self.verify_stream_hashes()
766781

767-
def __len__(self):
782+
def __len__(self) -> int:
768783
"""
769-
the length of a collection is the number of streams
784+
The length of a collection is the number of streams.
770785
"""
771786
return len(self.get_stream_names())
772787

773-
def verify_stream_hashes(self):
788+
def verify_stream_hashes(self) -> None:
774789
"""
775-
compares the stream hashes in the collection metadata to the metadata files
790+
Compares the stream hashes in the collection metadata to the metadata files.
791+
792+
Raises
793+
------
794+
SigMFFileError
795+
If any dataset checksums do not match saved metadata.
776796
"""
777797
streams = self.get_collection_field(self.STREAMS_KEY, [])
778798
for stream in streams:
779799
old_hash = stream.get("hash")
780800
metafile_name = get_sigmf_filenames(stream.get("name"))["meta_fn"]
781-
if path.isfile(metafile_name):
782-
new_hash = sigmf_hash.calculate_sha512(filename=metafile_name)
801+
metafile_path = self.base_path / metafile_name
802+
if path.isfile(metafile_path):
803+
new_hash = sigmf_hash.calculate_sha512(filename=metafile_path)
783804
if old_hash != new_hash:
784-
raise SigMFFileError(f"Calculated file hash for {metafile_name} does not match collection metadata.")
805+
raise SigMFFileError(
806+
f"Calculated file hash for {metafile_path} does not match collection metadata."
807+
)
785808

786-
def set_streams(self, metafiles):
809+
def set_streams(self, metafiles) -> None:
787810
"""
788-
configures the collection `core:streams` field from the specified list of metafiles
811+
Configures the collection `core:streams` field from the specified list of metafiles.
789812
"""
790813
self.metafiles = metafiles
791814
streams = []
792815
for metafile in self.metafiles:
793-
if metafile.endswith(".sigmf-meta") and path.isfile(metafile):
816+
metafile_path = self.base_path / metafile
817+
if metafile.endswith(".sigmf-meta") and path.isfile(metafile_path):
794818
stream = {
795819
"name": get_sigmf_filenames(metafile)["base_fn"],
796-
"hash": sigmf_hash.calculate_sha512(filename=metafile),
820+
"hash": sigmf_hash.calculate_sha512(filename=metafile_path),
797821
}
798822
streams.append(stream)
799823
else:
800-
raise SigMFFileError(f"Specifed stream file {metafile} is not a valid SigMF Metadata file")
824+
raise SigMFFileError(f"Specifed stream file {metafile_path} is not a valid SigMF Metadata file")
801825
self.set_collection_field(self.STREAMS_KEY, streams)
802826

803-
def get_stream_names(self):
827+
def get_stream_names(self) -> list:
804828
"""
805-
returns a list of `name` object(s) from the `collection` level `core:streams` metadata
829+
Returns a list of `name` object(s) from the `collection` level `core:streams` metadata.
806830
"""
807831
return [s.get("name") for s in self.get_collection_field(self.STREAMS_KEY, [])]
808832

809-
def set_collection_info(self, new_collection):
833+
def set_collection_info(self, new_collection: dict) -> None:
810834
"""
811835
Overwrite the collection info with a new dictionary.
812836
"""
813837
self._metadata[self.COLLECTION_KEY] = new_collection.copy()
814838

815-
def get_collection_info(self):
839+
def get_collection_info(self) -> dict:
816840
"""
817841
Returns a dictionary with all the collection info.
818842
"""
@@ -821,19 +845,19 @@ def get_collection_info(self):
821845
except AttributeError:
822846
return {}
823847

824-
def set_collection_field(self, key, value):
848+
def set_collection_field(self, key: str, value) -> None:
825849
"""
826850
Inserts a value into the collection field.
827851
"""
828852
self._metadata[self.COLLECTION_KEY][key] = value
829853

830-
def get_collection_field(self, key, default=None):
854+
def get_collection_field(self, key: str, default=None):
831855
"""
832856
Return a field from the collection info, or default if the field is not set.
833857
"""
834858
return self._metadata[self.COLLECTION_KEY].get(key, default)
835859

836-
def tofile(self, file_path, pretty=True):
860+
def tofile(self, file_path, pretty: bool = True) -> None:
837861
"""
838862
Write metadata file
839863
@@ -844,10 +868,10 @@ def tofile(self, file_path, pretty=True):
844868
pretty : bool, default True
845869
When True will write more human-readable output, otherwise will be flat JSON.
846870
"""
847-
fns = get_sigmf_filenames(file_path)
848-
with open(fns["collection_fn"], "w") as fp:
849-
self.dump(fp, pretty=pretty)
850-
fp.write("\n") # text files should end in carriage return
871+
filenames = get_sigmf_filenames(file_path)
872+
with open(filenames["collection_fn"], "w") as handle:
873+
self.dump(handle, pretty=pretty)
874+
handle.write("\n") # text files should end in carriage return
851875

852876
def get_SigMFFile(self, stream_name=None, stream_index=None):
853877
"""
@@ -857,11 +881,11 @@ def get_SigMFFile(self, stream_name=None, stream_index=None):
857881
if stream_name is not None:
858882
if stream_name in self.get_stream_names():
859883
metafile = stream_name + ".sigmf_meta"
860-
if stream_index is not None and stream_index < self.__len__():
884+
if stream_index is not None and stream_index < len(self):
861885
metafile = self.get_stream_names()[stream_index] + ".sigmf_meta"
862-
863886
if metafile is not None:
864-
return fromfile(metafile, skip_checksum=self.skip_checksums)
887+
metafile_path = self.base_path / metafile
888+
return fromfile(metafile_path, skip_checksum=self.skip_checksums)
865889

866890

867891
def dtype_info(datatype):
@@ -1022,7 +1046,8 @@ def fromfile(filename, skip_checksum=False):
10221046
metadata = json.load(mdfile_reader)
10231047
collection_fp.close()
10241048

1025-
return SigMFCollection(metadata=metadata, skip_checksums=skip_checksum)
1049+
dir_path = path.split(meta_fn)[0]
1050+
return SigMFCollection(metadata=metadata, base_path=dir_path, skip_checksums=skip_checksum)
10261051

10271052
else:
10281053
meta_fp = open(meta_fn, "rb")

tests/test_collection.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Copyright: Multiple Authors
2+
#
3+
# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python
4+
#
5+
# SPDX-License-Identifier: LGPL-3.0-or-later
6+
7+
"""Tests for collections"""
8+
9+
import copy
10+
import os
11+
import shutil
12+
import tempfile
13+
import unittest
14+
from pathlib import Path
15+
16+
import numpy as np
17+
from hypothesis import given
18+
from hypothesis import strategies as st
19+
20+
from sigmf.archive import SIGMF_COLLECTION_EXT, SIGMF_DATASET_EXT, SIGMF_METADATA_EXT
21+
from sigmf.sigmffile import SigMFCollection, SigMFFile, fromfile
22+
23+
from .testdata import TEST_FLOAT32_DATA, TEST_METADATA
24+
25+
26+
class TestCollection(unittest.TestCase):
27+
"""unit tests for colections"""
28+
29+
def setUp(self):
30+
"""create temporary path"""
31+
self.temp_dir = Path(tempfile.mkdtemp())
32+
33+
def tearDown(self):
34+
"""remove temporary path"""
35+
shutil.rmtree(self.temp_dir)
36+
37+
@given(st.sampled_from([".", "subdir/", "sub0/sub1/sub2/"]))
38+
def test_load_collection(self, subdir: str) -> None:
39+
"""test path handling for collections"""
40+
data_name1 = "dat1" + SIGMF_DATASET_EXT
41+
data_name2 = "dat2" + SIGMF_DATASET_EXT
42+
meta_name1 = "dat1" + SIGMF_METADATA_EXT
43+
meta_name2 = "dat2" + SIGMF_METADATA_EXT
44+
collection_name = "collection" + SIGMF_COLLECTION_EXT
45+
data_path1 = self.temp_dir / subdir / data_name1
46+
data_path2 = self.temp_dir / subdir / data_name2
47+
meta_path1 = self.temp_dir / subdir / meta_name1
48+
meta_path2 = self.temp_dir / subdir / meta_name2
49+
collection_path = self.temp_dir / subdir / collection_name
50+
os.makedirs(collection_path.parent, exist_ok=True)
51+
52+
# create data files
53+
TEST_FLOAT32_DATA.tofile(data_path1)
54+
TEST_FLOAT32_DATA.tofile(data_path2)
55+
56+
# create metadata files
57+
metadata = copy.deepcopy(TEST_METADATA)
58+
meta1 = SigMFFile(metadata=metadata, data_file=data_path1)
59+
meta2 = SigMFFile(metadata=metadata, data_file=data_path2)
60+
meta1.tofile(meta_path1)
61+
meta2.tofile(meta_path2)
62+
63+
# create collection
64+
collection = SigMFCollection(
65+
metafiles=[meta_name1, meta_name2],
66+
base_path=str(self.temp_dir / subdir),
67+
)
68+
collection.tofile(collection_path)
69+
70+
# load collection
71+
collection_loopback = fromfile(collection_path)
72+
meta1_loopback = collection_loopback.get_SigMFFile(stream_index=0)
73+
meta2_loopback = collection_loopback.get_SigMFFile(stream_index=1)
74+
75+
self.assertTrue(np.array_equal(TEST_FLOAT32_DATA, meta1_loopback.read_samples()))
76+
self.assertTrue(np.array_equal(TEST_FLOAT32_DATA, meta2_loopback[:]))

0 commit comments

Comments
 (0)