Skip to content

Commit e21fca8

Browse files
committed
have blue keywords parsed into fixed header; type hinting
1 parent 57d453f commit e21fca8

5 files changed

Lines changed: 39 additions & 53 deletions

File tree

docs/source/converters.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ All converters return a :class:`~sigmf.SigMFFile` object with converted metadata
1919
Fromfile Auto-Detection
2020
~~~~~~~~~~~~~~~~~~~~~~~
2121

22-
The :func:`~sigmf.sigmffile.fromfile` function automatically detects file formats and creates Non-Conforming Datasets:
22+
The :func:`~sigmf.sigmffile.fromfile` function automatically detects input file
23+
formats and reads without writing any output files:
2324

2425
.. code-block:: python
2526
@@ -100,7 +101,6 @@ The BLUE converter handles CDIF (.cdif) recordings while placing BLUE header inf
100101
* ``blue:fixed`` - Fixed header information (at start of file).
101102
* ``blue:adjunct`` - Adjunct header information (after fixed header).
102103
* ``blue:extended`` - Extended header information (at end of file). Note any duplicate fields will have a suffix like ``_1``, ``_2``, etc appended.
103-
* ``blue:keywords`` - User-defined key-value pairs.
104104

105105
.. autofunction:: sigmf.convert.blue.blue_to_sigmf
106106

@@ -123,7 +123,7 @@ Examples
123123
124124
# access BLUE-specific metadata
125125
blue_type = meta.get_global_field("blue:fixed")["type"] # e.g., 1000
126-
blue_version = meta.get_global_field("blue:keywords")["IO"] # e.g., "X-Midas"
126+
blue_version = meta.get_global_field("blue:fixed")["keywords"]["IO"] # e.g., "X-Midas"
127127
128128
Tested Formats
129129
~~~~~~~~~~~~~~

sigmf/convert/blue.py

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
}
8181

8282

83-
def blue_to_sigmf_type_str(h_fixed):
83+
def blue_to_sigmf_type_str(h_fixed: dict) -> str:
8484
"""
8585
Convert BLUE format code to SigMF datatype string.
8686
@@ -121,7 +121,7 @@ def blue_to_sigmf_type_str(h_fixed):
121121
return datatype
122122

123123

124-
def detect_endian(data):
124+
def detect_endian(data: bytes) -> str:
125125
"""
126126
Detect endianness of a Bluefile header.
127127
@@ -149,15 +149,15 @@ def detect_endian(data):
149149
raise SigMFConversionError(f"Unsupported endianness: {endianness}")
150150

151151

152-
def read_hcb(file_path):
152+
def read_hcb(file_path: Path) -> (dict, dict):
153153
"""
154154
Read Header Control Block (HCB) from BLUE file.
155155
156156
First 256 bytes contains fixed header, followed by 256 bytes of adjunct header.
157157
158158
Parameters
159159
----------
160-
file_path : str
160+
file_path : Path
161161
Path to the Blue file.
162162
163163
Returns
@@ -166,8 +166,6 @@ def read_hcb(file_path):
166166
Fixed Header
167167
h_adjunct : dict
168168
Adjunct Header
169-
h_keywords : dict
170-
Custom User Keywords parsed from fixed header.
171169
172170
Raises
173171
------
@@ -198,6 +196,8 @@ def read_hcb(file_path):
198196
if "=" in field:
199197
key, value = field.split("=", 1)
200198
h_keywords[key] = value
199+
# place parsed keywords back into fixed header
200+
h_fixed["keywords"] = h_keywords
201201

202202
# variable (adjunct) header parsing
203203
if h_fixed["type"] in (1000, 1001):
@@ -225,10 +225,10 @@ def read_hcb(file_path):
225225
validate_fixed(h_fixed)
226226
validate_adjunct(h_adjunct)
227227

228-
return h_fixed, h_adjunct, h_keywords
228+
return h_fixed, h_adjunct
229229

230230

231-
def read_extended_header(file_path, h_fixed):
231+
def read_extended_header(file_path: Path, h_fixed: dict) -> list:
232232
"""
233233
Read Extended Header from a BLUE file.
234234
@@ -382,13 +382,13 @@ def _get_data_boundaries(blue_path: Path, h_fixed: dict) -> (int, int):
382382
return header_bytes, data_bytes, trailing_bytes
383383

384384

385-
def _description(h_fixed: dict, h_keywords: dict) -> str:
385+
def _description(h_fixed: dict) -> str:
386386
"""
387387
Construct a human-readable description of the BLUE file.
388388
"""
389389
try:
390390
spec_str = "Unknown"
391-
version = Version(h_keywords.get("VER", "0.0"))
391+
version = Version(h_fixed.get("keywords").get("VER", "0.0"))
392392
if version.major == 1:
393393
spec_str = f"BLUE {version}"
394394
elif version.major == 2:
@@ -406,7 +406,6 @@ def _description(h_fixed: dict, h_keywords: dict) -> str:
406406

407407
def _build_common_metadata(
408408
h_fixed: dict,
409-
h_keywords: dict,
410409
h_adjunct: dict,
411410
h_extended: list,
412411
is_ncd: bool = False,
@@ -420,8 +419,6 @@ def _build_common_metadata(
420419
----------
421420
h_fixed : dict
422421
Fixed Header
423-
h_keywords : dict
424-
Custom User Keywords
425422
h_adjunct : dict
426423
Adjunct Header
427424
h_extended : list of dict
@@ -472,7 +469,7 @@ def get_tag(tag):
472469
SigMFFile.NUM_CHANNELS_KEY: num_channels,
473470
SigMFFile.SAMPLE_RATE_KEY: sample_rate_hz,
474471
SigMFFile.EXTENSIONS_KEY: [{"name": "blue", "version": "0.0.1", "optional": True}],
475-
SigMFFile.DESCRIPTION_KEY: _description(h_fixed, h_keywords),
472+
SigMFFile.DESCRIPTION_KEY: _description(h_fixed),
476473
}
477474

478475
# add NCD-specific fields
@@ -482,7 +479,6 @@ def get_tag(tag):
482479

483480
# merge HCB values into metadata
484481
global_info["blue:fixed"] = h_fixed
485-
global_info["blue:keywords"] = h_keywords
486482
global_info["blue:adjunct"] = h_adjunct
487483

488484
# merge extended header fields, handling duplicate keys
@@ -507,7 +503,7 @@ def get_tag(tag):
507503
# calculate blue start time
508504
blue_start_time = float(h_fixed.get("timecode", 0))
509505
blue_start_time += h_adjunct.get("xstart", 0)
510-
blue_start_time += float(h_keywords.get("TC_PREC", 0))
506+
blue_start_time += float(h_fixed.get("keywords").get("TC_PREC", 0))
511507

512508
capture_info = {}
513509

@@ -565,7 +561,7 @@ def validate_fixed(h_fixed: dict) -> None:
565561
SigMFConversionError
566562
If required fields are missing or invalid.
567563
"""
568-
required = ["version", "data_start", "data_size", "data_rep", "head_rep", "detached", "format", "type"]
564+
required = ["version", "data_start", "data_size", "data_rep", "head_rep", "detached", "format", "type", "keywords"]
569565
for field in required:
570566
if field not in h_fixed:
571567
raise SigMFConversionError(f"Missing required Fixed Header field: {field}")
@@ -619,7 +615,6 @@ def validate_extended(entries: list) -> None:
619615
def construct_sigmf(
620616
filenames: dict,
621617
h_fixed: dict,
622-
h_keywords: dict,
623618
h_adjunct: dict,
624619
h_extended: list,
625620
is_metadata_only: bool = False,
@@ -634,8 +629,6 @@ def construct_sigmf(
634629
Mapping returned by get_sigmf_filenames containing destination paths.
635630
h_fixed : dict
636631
Fixed Header
637-
h_keywords : dict
638-
Custom User Keywords
639632
h_adjunct : dict
640633
Adjunct Header
641634
h_extended : list of dict
@@ -651,7 +644,7 @@ def construct_sigmf(
651644
SigMF object.
652645
"""
653646
# use shared helper to build common metadata
654-
global_info, capture_info = _build_common_metadata(h_fixed, h_keywords, h_adjunct, h_extended)
647+
global_info, capture_info = _build_common_metadata(h_fixed, h_adjunct, h_extended)
655648

656649
# set metadata-only flag for zero-sample files (only for non-NCD files)
657650
if is_metadata_only:
@@ -694,7 +687,6 @@ def construct_sigmf(
694687
def construct_sigmf_ncd(
695688
blue_path: Path,
696689
h_fixed: dict,
697-
h_keywords: dict,
698690
h_adjunct: dict,
699691
h_extended: list,
700692
) -> SigMFFile:
@@ -707,8 +699,6 @@ def construct_sigmf_ncd(
707699
Path to the original BLUE file.
708700
h_fixed : dict
709701
Fixed Header
710-
h_keywords : dict
711-
Custom User Keywords
712702
h_adjunct : dict
713703
Adjunct Header
714704
h_extended : list of dict
@@ -724,7 +714,6 @@ def construct_sigmf_ncd(
724714
# use shared helper to build common metadata, with NCD-specific additions
725715
global_info, capture_info = _build_common_metadata(
726716
h_fixed,
727-
h_keywords,
728717
h_adjunct,
729718
h_extended,
730719
is_ncd=True,
@@ -790,7 +779,7 @@ def blue_to_sigmf(
790779
validate_file(blue_path)
791780

792781
# read Header control block (HCB) to determine how to process the rest of the file
793-
h_fixed, h_adjunct, h_keywords = read_hcb(blue_path)
782+
h_fixed, h_adjunct = read_hcb(blue_path)
794783

795784
# read extended header
796785
h_extended = read_extended_header(blue_path, h_fixed)
@@ -802,7 +791,7 @@ def blue_to_sigmf(
802791
# handle NCD case
803792
if create_ncd:
804793
# create metadata-only SigMF for NCD pointing to original file
805-
ncd_meta = construct_sigmf_ncd(blue_path, h_fixed, h_keywords, h_adjunct, h_extended)
794+
ncd_meta = construct_sigmf_ncd(blue_path, h_fixed, h_adjunct, h_extended)
806795

807796
# write NCD metadata to specified output path if provided
808797
if out_path is not None:
@@ -828,7 +817,6 @@ def blue_to_sigmf(
828817
meta = construct_sigmf(
829818
filenames=filenames,
830819
h_fixed=h_fixed,
831-
h_keywords=h_keywords,
832820
h_adjunct=h_adjunct,
833821
h_extended=h_extended,
834822
is_metadata_only=metadata_only,
@@ -839,9 +827,6 @@ def blue_to_sigmf(
839827
for key, _, _, _, desc in FIXED_LAYOUT:
840828
log.debug(f"{key:10s}: {h_fixed[key]!r} # {desc}")
841829

842-
log.debug(">>>>>>>>> User Keywords")
843-
log.debug(h_keywords)
844-
845830
log.debug(">>>>>>>>> Adjunct Header")
846831
log.debug(h_adjunct)
847832

tests/test_convert_blue.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from sigmf.convert.blue import blue_to_sigmf
1717

1818
from .test_convert_wav import _validate_ncd
19-
from .testdata import NONSIGMF_ENV, NONSIGMF_REPO
19+
from .testdata import get_nonsigmf_path
2020

2121

2222
class TestBlueWithNonSigMFRepo(unittest.TestCase):
@@ -26,12 +26,9 @@ def setUp(self) -> None:
2626
"""setup paths to blue files"""
2727
self.tmp_dir = tempfile.TemporaryDirectory()
2828
self.tmp_path = Path(self.tmp_dir.name)
29-
if not NONSIGMF_REPO:
30-
# skip test if environment variable not set
31-
self.skipTest(f"Set {NONSIGMF_ENV} environment variable to path with BLUE files to run test.")
32-
29+
nonsigmf_path = get_nonsigmf_path(self)
3330
# glob all files in blue/ directory
34-
blue_dir = NONSIGMF_REPO / "blue"
31+
blue_dir = nonsigmf_path / "blue"
3532
self.blue_paths = []
3633
if blue_dir.exists():
3734
for ext in ["*.cdif", "*.tmp"]:

tests/test_convert_wav.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
import sigmf
1717
from sigmf.convert.wav import wav_to_sigmf
1818

19-
from .testdata import NONSIGMF_ENV, NONSIGMF_REPO
19+
from .testdata import get_nonsigmf_path
2020

2121

22-
def _validate_ncd(test, meta, target_path):
22+
def _validate_ncd(test: unittest.TestCase, meta: sigmf.SigMFFile, target_path: Path):
2323
"""non-conforming dataset has a specific structure"""
2424
test.assertEqual(str(meta.data_file), str(target_path), "Auto-detected NCD should point to original file")
2525
test.assertIsInstance(meta, sigmf.SigMFFile)
@@ -107,12 +107,9 @@ def setUp(self) -> None:
107107
"""setup paths to example wav files"""
108108
self.tmp_dir = tempfile.TemporaryDirectory()
109109
self.tmp_path = Path(self.tmp_dir.name)
110-
if not NONSIGMF_REPO:
111-
# skip test if environment variable not set
112-
self.skipTest(f"Set {NONSIGMF_ENV} environment variable to path with WAV files to run test.")
113-
110+
nonsigmf_path = get_nonsigmf_path(self)
114111
# glob all files in wav/ directory
115-
wav_dir = NONSIGMF_REPO / "wav"
112+
wav_dir = nonsigmf_path / "wav"
116113
self.wav_paths = []
117114
if wav_dir.exists():
118115
self.wav_paths = list(wav_dir.glob("*.wav"))

tests/testdata.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,25 @@
77
"""Shared test data for tests."""
88

99
import os
10+
import unittest
1011
from pathlib import Path
1112

1213
import numpy as np
1314

1415
from sigmf import SigMFFile, __specification__, __version__
1516

16-
# detection for https://github.com/sigmf/example_nonsigmf_recordings
17-
NONSIGMF_ENV = "EXAMPLE_NONSIGMF_RECORDINGS_PATH"
18-
NONSIGMF_REPO = None
19-
_recordings_path = Path(os.getenv(NONSIGMF_ENV, "nopath"))
20-
if _recordings_path.is_dir():
21-
NONSIGMF_REPO = Path(_recordings_path)
17+
18+
def get_nonsigmf_path(test: unittest.TestCase) -> Path:
19+
"""Get path to example_nonsigmf_recordings repo or skip test"""
20+
nonsigmf_env = "EXAMPLE_NONSIGMF_RECORDINGS_PATH"
21+
recordings_path = Path(os.getenv(nonsigmf_env, "nopath"))
22+
if not recordings_path.is_dir():
23+
test.skipTest(
24+
f"Set {nonsigmf_env} environment variable to path non-SigMF recordings repository to run test."
25+
f" Available at https://github.com/sigmf/example_nonsigmf_recordings"
26+
)
27+
return recordings_path
28+
2229

2330
TEST_FLOAT32_DATA = np.arange(16, dtype=np.float32)
2431
TEST_METADATA = {

0 commit comments

Comments
 (0)