From c5d194d5e659def926d25737baa7b6cbbb4887bd Mon Sep 17 00:00:00 2001 From: Teque5 Date: Wed, 14 Feb 2024 10:36:45 -0800 Subject: [PATCH 1/4] Drop3.6 & move all configuration into pyproject.toml (#49) * move all configuration into pyproject.toml * tox configuration simplified and consolidated to pyproject.toml * default configuration for common tools (black, pytype, coverage) * add entry point for sigmf_convert_wav * slightly improve sigmf_convert_wav * increment to v1.2.0 * move tools/ to apps/ * move gui.py to apps/ * drop support for python 3.6 * add support for python 3.12 * distribution previously made with setup.py can be created w/python3 -m build * upgrade logo to SVG version * pin PySimpleGUI version --- .github/workflows/main.yml | 9 ++- .gitignore | 3 +- README.md | 27 +++++--- pyproject.toml | 102 ++++++++++++++++++++++++++++++ setup.cfg | 2 - setup.py | 47 -------------- sigmf/__init__.py | 2 +- sigmf/{tools => apps}/__init__.py | 0 sigmf/apps/convert_wav.py | 78 +++++++++++++++++++++++ sigmf/{ => apps}/gui.py | 4 +- sigmf/sigmffile.py | 12 ++-- sigmf/tools/wav2sigmf.py | 56 ---------------- sigmf/validate.py | 2 +- tox.ini | 22 ------- 14 files changed, 214 insertions(+), 152 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100755 setup.py rename sigmf/{tools => apps}/__init__.py (100%) create mode 100755 sigmf/apps/convert_wav.py rename sigmf/{ => apps}/gui.py (99%) delete mode 100755 sigmf/tools/wav2sigmf.py delete mode 100644 tox.ini diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b37b254..d7193d7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,6 @@ name: Python package -on: +on: push: pull_request: types: [opened, synchronize] @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.6"] + python-version: ["3.7", "3.9", "3.12"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} @@ -20,8 +20,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest - pip install . + pip install .[test,apps] - name: Test with pytest run: | - pytest + coverage run diff --git a/.gitignore b/.gitignore index 1ec0199..756579b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,9 @@ build/* .eggs/* SigMF.egg-info/* -# pytest & coverage related +# test related .coverage pytest.xml coverage.xml +.tox/ htmlcov/* diff --git a/README.md b/README.md index 790a8ff..56242e8 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,21 @@ -

+

Rendered SigMF Logo

This python module makes it easy to interact with Signal Metadata Format -(SigMF) objects. This module works with Python 3.6+ and is distributed freely -under the terms GNU Lesser GPL v3 License. +(SigMF) recordings. This module works with Python 3.7+ and is distributed +freely under the terms GNU Lesser GPL v3 License. The [SigMF specification document](https://github.com/sigmf/SigMF/blob/HEAD/sigmf-spec.md) is located in the [SigMF](https://github.com/gnuradio/SigMF) repository. # Installation -To install the latest release, install from pip: +To install the latest PyPi release, install from pip: ```bash pip install sigmf ``` -To install the latest development version, build from source: +To install the latest git release, build from source: ```bash git clone https://github.com/sigmf/sigmf-python.git @@ -23,12 +23,19 @@ cd sigmf-python pip install . ``` -To run the included QA tests: +Testing can be run with a variety of tools: + ```bash -# basic -python3 -m pytest tests/ -# fancy -coverage run --a --source sigmf -m pytest --doctest-modules +# pytest and coverage run locally +pytest +coverage run +# run coverage in a venv +tox run +# other useful tools +pylint sigmf tests +pytype +black +flake8 ``` # Examples diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ee425f7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,102 @@ +[project] +name = "SigMF" +description = "Easily interact with Signal Metadata Format (SigMF) recordings." +keywords = ["gnuradio"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dynamic = ["version", "readme"] +requires-python = ">=3.7" +dependencies = [ + "numpy", # for vector math + "jsonschema", # for spec validation +] + [project.urls] + repository = "https://github.com/sigmf/sigmf-python" + [project.scripts] + sigmf_validate = "sigmf.validate:main" + sigmf_gui = "sigmf.apps.gui:main [apps]" + sigmf_convert_wav = "sigmf.apps.convert_wav:main [apps]" + [project.optional-dependencies] + test = [ + "pylint", + "pytest", + "pytest-cov", + "hypothesis", # next-gen testing framework + ] + apps = [ + "scipy", # for wav i/o + # FIXME: PySimpleGUI 2024-02-12 v5.0.0 release seems to have a bug. Unpin version when possible. + "PySimpleGUI < 5.0.0", # for gui interface + ] + +[tool.setuptools] +packages = ["sigmf"] + [tool.setuptools.dynamic] + version = {attr = "sigmf.__version__"} + readme = {file = ["README.md"], content-type = "text/markdown"} + [tool.setuptools.package-data] + sigmf = ["*.json"] + +[build-system] +requires = ["setuptools>=65.0", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[tool.coverage.run] +branch = true +source = ["sigmf", "tests"] +# -rA captures stdout from all tests and places it after the pytest summary +command_line = "-m pytest -rA --doctest-modules --junitxml=pytest.xml" + +[tool.pytest.ini_options] +addopts = "--doctest-modules" + +[tool.pylint] + [tool.pylint.main] + load-plugins = [ + "pylint.extensions.typing", + "pylint.extensions.docparams", + ] + exit-zero = true + [tool.pylint.messages_control] + disable = [ + "logging-not-lazy", + "missing-module-docstring", + "import-error", + "unspecified-encoding", + ] + max-line-length = 120 + [tool.pylint.REPORTS] + # omit from the similarity reports + ignore-comments = 'yes' + ignore-docstrings = 'yes' + ignore-imports = 'yes' + ignore-signatures = 'yes' + min-similarity-lines = 4 + +[tool.pytype] +inputs = ['sigmf', 'tests'] + +[tool.black] +line-length = 120 + +[tool.tox] +legacy_tox_ini = ''' + [tox] + skip_missing_interpreters = True + envlist = py{37,38,39,310,311,312} + + [testenv] + usedevelop = True + deps = .[test,apps] + commands = coverage run +''' diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b7e4789..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[aliases] -test=pytest diff --git a/setup.py b/setup.py deleted file mode 100755 index 1d43b36..0000000 --- a/setup.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -from setuptools import setup -import os -import re - -short_description = "Python module for interacting with SigMF recordings." - -with open("README.md", encoding="utf-8") as handle: - long_description = handle.read() - -with open(os.path.join("sigmf", "__init__.py"), encoding="utf-8") as handle: - version = re.search(r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]', handle.read()).group(1) - -setup( - name="SigMF", - version=version, - description=short_description, - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/sigmf/sigmf-python", - license="GNU Lesser General Public License v3 or later (LGPLv3+)", - classifiers=[ - "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - ], - entry_points={ - "console_scripts": [ - "sigmf_validate = sigmf.validate:main", - "sigmf_gui = sigmf.gui:main [gui]", - ] - }, - packages=["sigmf"], - package_data={ - "sigmf": ["*.json"], - }, - install_requires=["numpy", "jsonschema"], - extras_require={"gui": "pysimplegui==4.0.0"}, - setup_requires=["pytest-runner"], - tests_require=["pytest>3", "hypothesis"], - zip_safe=False, -) diff --git a/sigmf/__init__.py b/sigmf/__init__.py index fa4275e..e11844a 100644 --- a/sigmf/__init__.py +++ b/sigmf/__init__.py @@ -4,7 +4,7 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -__version__ = "1.1.5" +__version__ = "1.2.0" from .archive import SigMFArchive from .sigmffile import SigMFFile, SigMFCollection diff --git a/sigmf/tools/__init__.py b/sigmf/apps/__init__.py similarity index 100% rename from sigmf/tools/__init__.py rename to sigmf/apps/__init__.py diff --git a/sigmf/apps/convert_wav.py b/sigmf/apps/convert_wav.py new file mode 100755 index 0000000..638bd46 --- /dev/null +++ b/sigmf/apps/convert_wav.py @@ -0,0 +1,78 @@ +# Copyright: Multiple Authors +# +# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +"""converter for wav containers""" + +import os +import tempfile +import datetime +import pathlib +import argparse +import getpass + +from scipy.io import wavfile + +from .. import archive +from ..sigmffile import SigMFFile +from ..utils import get_data_type_str + + +def convert_wav(input_wav_filename, archive_filename=None, start_datetime=None, author=None): + """ + read a .wav and write a .sigmf archive + """ + samp_rate, wav_data = wavfile.read(input_wav_filename) + + global_info = { + SigMFFile.AUTHOR_KEY: getpass.getuser() if author is None else author, + SigMFFile.DATATYPE_KEY: get_data_type_str(wav_data), + SigMFFile.DESCRIPTION_KEY: f"Converted from {input_wav_filename}", + SigMFFile.NUM_CHANNELS_KEY: 1 if len(wav_data.shape) < 2 else wav_data.shape[1], + SigMFFile.RECORDER_KEY: os.path.basename(__file__), + SigMFFile.SAMPLE_RATE_KEY: samp_rate, + } + + if start_datetime is None: + fname = pathlib.Path(input_wav_filename) + mtime = datetime.datetime.fromtimestamp(fname.stat().st_mtime) + start_datetime = mtime.isoformat() + "Z" + + capture_info = {SigMFFile.START_INDEX_KEY: 0} + if start_datetime is not None: + capture_info[SigMFFile.DATETIME_KEY] = start_datetime + + tmpdir = tempfile.mkdtemp() + sigmf_data_filename = input_wav_filename + archive.SIGMF_DATASET_EXT + sigmf_data_path = os.path.join(tmpdir, sigmf_data_filename) + wav_data.tofile(sigmf_data_path) + + meta = SigMFFile(data_file=sigmf_data_path, global_info=global_info) + meta.add_capture(0, metadata=capture_info) + + if archive_filename is None: + archive_filename = os.path.basename(input_wav_filename) + archive.SIGMF_ARCHIVE_EXT + meta.tofile(archive_filename, toarchive=True) + return os.path.abspath(archive_filename) + + +def main(): + """ + entry-point for sigmf_convert_wav + """ + parser = argparse.ArgumentParser(description="Convert .wav to .sigmf container.") + parser.add_argument("input", type=str, help="Wavfile path") + parser.add_argument("--author", type=str, default=None, help=f"set {SigMFFile.AUTHOR_KEY} metadata") + args = parser.parse_args() + + out_fname = convert_wav( + input_wav_filename=args.input, + author=args.author, + ) + print("Wrote", out_fname) + + +if __name__ == "__main__": + main() diff --git a/sigmf/gui.py b/sigmf/apps/gui.py similarity index 99% rename from sigmf/gui.py rename to sigmf/apps/gui.py index 7d44b86..ac51da3 100644 --- a/sigmf/gui.py +++ b/sigmf/apps/gui.py @@ -10,8 +10,8 @@ import logging from PySimpleGUI import * -from .sigmffile import SigMFFile, fromarchive, dtype_info -from .archive import SIGMF_ARCHIVE_EXT +from ..sigmffile import SigMFFile, fromarchive, dtype_info +from ..archive import SIGMF_ARCHIVE_EXT log = logging.getLogger() diff --git a/sigmf/sigmffile.py b/sigmf/sigmffile.py index bd68118..9e72d01 100644 --- a/sigmf/sigmffile.py +++ b/sigmf/sigmffile.py @@ -202,7 +202,7 @@ def __next__(self): def __getitem__(self, sli): mem = self._memmap[sli] # matches behavior of numpy.ndarray.__getitem__() - + if self._return_type is None: return mem @@ -229,13 +229,15 @@ def get_num_channels(self): def _is_conforming_dataset(self): """ - Returns `True` if the dataset is conforming to SigMF, `False` otherwise - The dataset is non-conforming if the datafile contains non-sample bytes which means global trailing_bytes field is zero or not set, all captures `header_bytes` fields are zero or not set. Because we do not necessarily know the filename no means of verifying the meta/data filename roots match, but this will also check that a data file exists. + + Returns + ------- + `True` if the dataset is conforming to SigMF, `False` otherwise """ if self.get_global_field(self.TRAILING_BYTES_KEY, 0): return False @@ -405,7 +407,7 @@ def get_annotations(self, index=None): annotations = self._metadata.get(self.ANNOTATION_KEY, []) if index is None: return annotations - + annotations_including_index = [] for annotation in annotations: if index < annotation[self.START_INDEX_KEY]: @@ -416,7 +418,7 @@ def get_annotations(self, index=None): if index >= annotation[self.START_INDEX_KEY] + annotation[self.LENGTH_INDEX_KEY]: # index is after annotation end -> skip continue - + annotations_including_index.append(annotation) return annotations_including_index diff --git a/sigmf/tools/wav2sigmf.py b/sigmf/tools/wav2sigmf.py deleted file mode 100755 index 99e9ee3..0000000 --- a/sigmf/tools/wav2sigmf.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 - -import os, tempfile -from scipy.io import wavfile -import sigmf -from sigmf import SigMFFile, SigMFArchive -from sigmf.utils import get_data_type_str - -def writeSigMFArchiveFromWave(input_wav_filename, archive_filename=None, start_datetime=None, author=None): - samplerate, wav_data = wavfile.read(input_wav_filename) - - global_info = { - SigMFFile.DATATYPE_KEY: get_data_type_str(wav_data), - SigMFFile.SAMPLE_RATE_KEY: samplerate, - SigMFFile.DESCRIPTION_KEY: 'Converted from ' + input_wav_filename + '.', - SigMFFile.NUM_CHANNELS_KEY: 1 if len(wav_data.shape) < 2 else wav_data.shape[1], - SigMFFile.RECORDER_KEY: os.path.basename(__file__), - } - if author is None: - try: - import getpass - except: - pass - else: - author = getpass.getuser() - if author is not None: - global_info[SigMFFile.AUTHOR_KEY]: author - - if start_datetime is None: - import datetime, pathlib - fname = pathlib.Path(input_wav_filename) - mtime = datetime.datetime.fromtimestamp(fname.stat().st_mtime) - start_datetime = mtime.isoformat() + 'Z' - - capture_info = {SigMFFile.START_INDEX_KEY: 0} - if start_datetime is not None: - capture_info[SigMFFile.DATETIME_KEY] = start_datetime - - tmpdir = tempfile.mkdtemp() - sigmf_data_filename = input_wav_filename + sigmf.archive.SIGMF_DATASET_EXT - sigmf_data_path = os.path.join(tmpdir, sigmf_data_filename) - wav_data.tofile(sigmf_data_path) - - meta = sigmf.SigMFFile(data_file=sigmf_data_path, global_info=global_info) - meta.add_capture(0, metadata=capture_info) - - if archive_filename is None: - archive_filename = os.path.basename(input_wav_filename) + sigmf.archive.SIGMF_ARCHIVE_EXT - meta.tofile(archive_filename, toarchive=True) - return os.path.abspath(archive_filename) - -if __name__ == '__main__': - import sys - input_wav_filename = sys.argv[1] # produces an understandable error if nothing was provided on command line - out_fname = writeSigMFArchiveFromWave(input_wav_filename) - print("Wrote", out_fname) diff --git a/sigmf/validate.py b/sigmf/validate.py index a251bdd..ce18fcc 100644 --- a/sigmf/validate.py +++ b/sigmf/validate.py @@ -124,5 +124,5 @@ def main(): log.info('Validation OK!') -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 02a42f5..0000000 --- a/tox.ini +++ /dev/null @@ -1,22 +0,0 @@ -[tox] -skip_missing_interpreters = True -envlist = py36, py37, py38, py39, py310 - -[testenv] -usedevelop = True -deps = - pytest - flake8 -commands = - pytest - - flake8 - -[testenv:coverage] -deps = - pytest-cov -commands = py.test --cov-report term-missing --cov=sigmf tests - -[flake8] -max-line-length = 120 -[pycodestyle] -max-line-length = 120 From 24108743524ba3b5be9f39a4ea25028e8b3a137a Mon Sep 17 00:00:00 2001 From: Liam Beguin Date: Fri, 12 Apr 2024 00:18:45 -0400 Subject: [PATCH 2/4] utils: iso8601: strip fractional seconds (#56) According to ISO8601 any number of digits is permitted for fractional seconds, the issue is that Python assumes these are microsecond, and will fail if we have more than 6 digits. In the event that we detect more that 6 digits, truncate the fractional part and pass it to datetime. Signed-off-by: Liam Beguin --- sigmf/utils.py | 8 ++++++++ tests/test_utils.py | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 tests/test_utils.py diff --git a/sigmf/utils.py b/sigmf/utils.py index 5f54e1f..18c7cf8 100644 --- a/sigmf/utils.py +++ b/sigmf/utils.py @@ -10,6 +10,7 @@ from datetime import datetime import sys import numpy as np +import re from . import error @@ -30,6 +31,13 @@ def parse_iso8601_datetime(datestr: str) -> datetime: >>> parse_iso8601_datetime("1955-11-05T06:15:00Z") datetime.datetime(1955, 11, 5, 6, 15) """ + # provided string exceeds max precision -> truncate to µs + match = re.match(r"^(?P
.*)(?P\.[0-9]{7,})Z$", datestr) + if match: + md = match.groupdict() + length = min(7, len(md["frac"])) + datestr = ''.join([md["dt"], md["frac"][:length], "Z"]) + try: timestamp = datetime.strptime(datestr, '%Y-%m-%dT%H:%M:%S.%fZ') except ValueError: diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..9a98ed4 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,19 @@ +from datetime import datetime + +import pytest + +from sigmf import utils + + +@pytest.mark.parametrize("ts, expected", [ + ("1955-07-04T05:15:00Z", datetime(year=1955, month=7, day=4, hour=5, minute=15, second=00, microsecond=0)), + ("2956-08-05T06:15:12Z", datetime(year=2956, month=8, day=5, hour=6, minute=15, second=12, microsecond=0)), + ("3957-09-06T07:15:12.345Z", datetime(year=3957, month=9, day=6, hour=7, minute=15, second=12, microsecond=345000)), + ("4958-10-07T08:15:12.0345Z", datetime(year=4958, month=10, day=7, hour=8, minute=15, second=12, microsecond=34500)), + ("5959-11-08T09:15:12.000000Z", datetime(year=5959, month=11, day=8, hour=9, minute=15, second=12, microsecond=0)), + ("6960-12-09T10:15:12.123456789123Z", datetime(year=6960, month=12, day=9, hour=10, minute=15, second=12, microsecond=123456)), + +]) +def test_parse_simple_iso8601(ts, expected): + dt = utils.parse_iso8601_datetime(ts) + assert dt == expected From 1b03f636219c7fbe7ce226f396dffe30061fa241 Mon Sep 17 00:00:00 2001 From: Josh Bailey Date: Fri, 12 Apr 2024 16:26:01 +1200 Subject: [PATCH 3/4] Validate that global core:version is present. (#55) Do not overwrite version info when reading files. --- sigmf/sigmffile.py | 5 +---- tests/conftest.py | 1 + tests/test_archivereader.py | 1 + tests/test_validation.py | 6 ++++++ 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/sigmf/sigmffile.py b/sigmf/sigmffile.py index 9e72d01..91a31b7 100644 --- a/sigmf/sigmffile.py +++ b/sigmf/sigmffile.py @@ -174,6 +174,7 @@ def __init__(self, metadata=None, data_file=None, global_info=None, skip_checksu if metadata is None: self._metadata = {self.GLOBAL_KEY:{}, self.CAPTURE_KEY:[], self.ANNOTATION_KEY:[]} self._metadata[self.GLOBAL_KEY][self.NUM_CHANNELS_KEY] = 1 + self._metadata[self.GLOBAL_KEY][self.VERSION_KEY] = "1.0.0" elif isinstance(metadata, dict): self._metadata = metadata else: @@ -183,8 +184,6 @@ def __init__(self, metadata=None, data_file=None, global_info=None, skip_checksu if data_file is not None: self.set_data_file(data_file, skip_checksum=skip_checksum, map_readonly=map_readonly) - self._metadata[self.GLOBAL_KEY][self.VERSION_KEY] = '1.0.0' - def __len__(self): return self._memmap.shape[0] @@ -724,8 +723,6 @@ def __init__(self, metafiles=None, metadata=None, skip_checksums=False): if not self.skip_checksums: self.verify_stream_hashes() - self._metadata[self.COLLECTION_KEY][self.VERSION_KEY] = '1.0.0' - def __len__(self): ''' the length of a collection is the number of streams diff --git a/tests/conftest.py b/tests/conftest.py index d25ad06..f8f6aa4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,6 +40,7 @@ def test_sigmffile(test_data_file): """If pytest uses this signature, will return valid SigMF file.""" sigf = SigMFFile() sigf.set_global_field("core:datatype", "rf32_le") + sigf.set_global_field("core:version", "1.0.0") sigf.add_annotation(start_index=0, length=len(TEST_FLOAT32_DATA)) sigf.add_capture(start_index=0) sigf.set_data_file(test_data_file.name) diff --git a/tests/test_archivereader.py b/tests/test_archivereader.py index 657d633..1818285 100644 --- a/tests/test_archivereader.py +++ b/tests/test_archivereader.py @@ -42,6 +42,7 @@ def test_access_data_without_untar(self): global_info={ SigMFFile.DATATYPE_KEY: f"{complex_prefix}{key}_le", SigMFFile.NUM_CHANNELS_KEY: num_channels, + SigMFFile.VERSION_KEY: "1.0.0", }, ) temp_meta.tofile(temp_archive, toarchive=True) diff --git a/tests/test_validation.py b/tests/test_validation.py index c2c267b..ce427a1 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -39,6 +39,12 @@ class FailingCases(unittest.TestCase): def setUp(self): self.metadata = dict(TEST_METADATA) + def test_no_version(self): + '''core:version must be present''' + del self.metadata[SigMFFile.GLOBAL_KEY][SigMFFile.VERSION_KEY] + with self.assertRaises(ValidationError): + SigMFFile(self.metadata).validate() + def test_extra_top_level_key(self): '''no extra keys allowed on the top level''' self.metadata['extra'] = 0 From ccc1cbfe445fe859aae272690bf9a8589d493472 Mon Sep 17 00:00:00 2001 From: Teque5 Date: Mon, 15 Apr 2024 10:01:17 -0700 Subject: [PATCH 4/4] Track Specification within Repo & Polish (#57) * Add new sigmf.__specification__ variable that tracks SigMF Spec release version * Fix logo visible in PyPi (only png works there, although svg does render on GitHub) * Replace outdated links from gnuradio to sigmf organization. * Adjust sigmf_convert_wav so that it reads xyz.wav and produces a simple xyz.sigmf. Also basic polish of implementation * Make license statement same across all files. --- README.md | 4 +- pyproject.toml | 4 +- sigmf/__init__.py | 11 ++- sigmf/apps/convert_wav.py | 37 ++++++--- sigmf/apps/gui.py | 17 ++-- sigmf/archive.py | 3 +- sigmf/archivereader.py | 8 +- sigmf/error.py | 2 +- sigmf/schema-collection.json | 4 +- sigmf/schema-meta.json | 2 +- sigmf/schema.py | 4 +- sigmf/sigmf_hash.py | 2 +- sigmf/sigmffile.py | 16 ++-- sigmf/utils.py | 7 +- sigmf/validate.py | 18 ++--- tests/conftest.py | 39 ++++------ tests/test_archive.py | 12 ++- tests/test_archivereader.py | 17 ++-- tests/test_sigmffile.py | 146 +++++++++++++++++------------------ tests/test_utils.py | 8 ++ tests/test_validation.py | 66 ++++++---------- tests/testdata.py | 89 +++++++++++---------- 22 files changed, 262 insertions(+), 254 deletions(-) diff --git a/README.md b/README.md index 56242e8..1837e60 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -

Rendered SigMF Logo

+

Rendered SigMF Logo

This python module makes it easy to interact with Signal Metadata Format (SigMF) recordings. This module works with Python 3.7+ and is distributed freely under the terms GNU Lesser GPL v3 License. The [SigMF specification document](https://github.com/sigmf/SigMF/blob/HEAD/sigmf-spec.md) -is located in the [SigMF](https://github.com/gnuradio/SigMF) repository. +is located in the [SigMF](https://github.com/sigmf/SigMF) repository. # Installation diff --git a/pyproject.toml b/pyproject.toml index ee425f7..15422cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "SigMF" description = "Easily interact with Signal Metadata Format (SigMF) recordings." -keywords = ["gnuradio"] +keywords = ["gnuradio", "radio"] classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", @@ -13,6 +13,8 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering", + "Topic :: Communications :: Ham Radio", ] dynamic = ["version", "readme"] requires-python = ">=3.7" diff --git a/sigmf/__init__.py b/sigmf/__init__.py index e11844a..38eed7b 100644 --- a/sigmf/__init__.py +++ b/sigmf/__init__.py @@ -1,19 +1,22 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later -__version__ = "1.2.0" +# version of this python module +__version__ = "1.2.1" +# matching version of the SigMF specification +__specification__ = "1.2.0" from .archive import SigMFArchive from .sigmffile import SigMFFile, SigMFCollection from .archivereader import SigMFArchiveReader from . import archive +from . import archivereader from . import error from . import schema from . import sigmffile -from . import validate from . import utils -from . import archivereader +from . import validate diff --git a/sigmf/apps/convert_wav.py b/sigmf/apps/convert_wav.py index 638bd46..9151d2f 100755 --- a/sigmf/apps/convert_wav.py +++ b/sigmf/apps/convert_wav.py @@ -1,29 +1,35 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later """converter for wav containers""" -import os -import tempfile -import datetime -import pathlib import argparse +import datetime import getpass +import logging +import os +import pathlib +import tempfile from scipy.io import wavfile +from .. import SigMFFile, __specification__ +from .. import __version__ as toolversion from .. import archive -from ..sigmffile import SigMFFile from ..utils import get_data_type_str +log = logging.getLogger() + def convert_wav(input_wav_filename, archive_filename=None, start_datetime=None, author=None): """ read a .wav and write a .sigmf archive """ + input_path = pathlib.Path(input_wav_filename) + input_stem = input_path.stem samp_rate, wav_data = wavfile.read(input_wav_filename) global_info = { @@ -33,11 +39,11 @@ def convert_wav(input_wav_filename, archive_filename=None, start_datetime=None, SigMFFile.NUM_CHANNELS_KEY: 1 if len(wav_data.shape) < 2 else wav_data.shape[1], SigMFFile.RECORDER_KEY: os.path.basename(__file__), SigMFFile.SAMPLE_RATE_KEY: samp_rate, + SigMFFile.VERSION_KEY: __specification__, } if start_datetime is None: - fname = pathlib.Path(input_wav_filename) - mtime = datetime.datetime.fromtimestamp(fname.stat().st_mtime) + mtime = datetime.datetime.fromtimestamp(input_path.stat().st_mtime) start_datetime = mtime.isoformat() + "Z" capture_info = {SigMFFile.START_INDEX_KEY: 0} @@ -45,7 +51,7 @@ def convert_wav(input_wav_filename, archive_filename=None, start_datetime=None, capture_info[SigMFFile.DATETIME_KEY] = start_datetime tmpdir = tempfile.mkdtemp() - sigmf_data_filename = input_wav_filename + archive.SIGMF_DATASET_EXT + sigmf_data_filename = input_stem + archive.SIGMF_DATASET_EXT sigmf_data_path = os.path.join(tmpdir, sigmf_data_filename) wav_data.tofile(sigmf_data_path) @@ -53,7 +59,7 @@ def convert_wav(input_wav_filename, archive_filename=None, start_datetime=None, meta.add_capture(0, metadata=capture_info) if archive_filename is None: - archive_filename = os.path.basename(input_wav_filename) + archive.SIGMF_ARCHIVE_EXT + archive_filename = input_stem + archive.SIGMF_ARCHIVE_EXT meta.tofile(archive_filename, toarchive=True) return os.path.abspath(archive_filename) @@ -65,13 +71,22 @@ def main(): parser = argparse.ArgumentParser(description="Convert .wav to .sigmf container.") parser.add_argument("input", type=str, help="Wavfile path") parser.add_argument("--author", type=str, default=None, help=f"set {SigMFFile.AUTHOR_KEY} metadata") + parser.add_argument('-v', '--verbose', action='count', default=0) + parser.add_argument('--version', action='version', version=f'%(prog)s v{toolversion}') args = parser.parse_args() + level_lut = { + 0: logging.WARNING, + 1: logging.INFO, + 2: logging.DEBUG, + } + logging.basicConfig(level=level_lut[min(args.verbose, 2)]) + out_fname = convert_wav( input_wav_filename=args.input, author=args.author, ) - print("Wrote", out_fname) + log.info(f"Write {out_fname}") if __name__ == "__main__": diff --git a/sigmf/apps/gui.py b/sigmf/apps/gui.py index ac51da3..9beda03 100644 --- a/sigmf/apps/gui.py +++ b/sigmf/apps/gui.py @@ -1,17 +1,20 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later '''GUI for creating & editing SigMF Files''' -import os +import argparse import logging +import os + from PySimpleGUI import * -from ..sigmffile import SigMFFile, fromarchive, dtype_info +from .. import __version__ as toolversion from ..archive import SIGMF_ARCHIVE_EXT +from ..sigmffile import SigMFFile, dtype_info, fromarchive log = logging.getLogger() @@ -381,13 +384,10 @@ def add_capture(capture_data_input, values, capture_selector_dict, file_data, fr def main(): - import argparse - from sigmf import __version__ as toolversion - parser = argparse.ArgumentParser(description='Edit SigMF Archive.') parser.add_argument('-i', '--input', help='Input SigMF Archive Path.', default=None) parser.add_argument('-v', '--verbose', action='count', default=0) - parser.add_argument('--version', action='version', version=f'%(prog)s {toolversion}') + parser.add_argument('--version', action='version', version=f'%(prog)s v{toolversion}') args = parser.parse_args() level_lut = { @@ -638,3 +638,6 @@ def main(): break window.Close() + +if __name__ == "__main__": + main() diff --git a/sigmf/archive.py b/sigmf/archive.py index de6bd50..897c0ee 100644 --- a/sigmf/archive.py +++ b/sigmf/archive.py @@ -1,6 +1,6 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later @@ -13,7 +13,6 @@ from .error import SigMFFileError - SIGMF_ARCHIVE_EXT = ".sigmf" SIGMF_METADATA_EXT = ".sigmf-meta" SIGMF_DATASET_EXT = ".sigmf-data" diff --git a/sigmf/archivereader.py b/sigmf/archivereader.py index 5759b74..556e84d 100644 --- a/sigmf/archivereader.py +++ b/sigmf/archivereader.py @@ -1,6 +1,6 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/gnuradio/SigMF +# This file is part of sigmf-python. https://github.com/sigmf/SigMF # # SPDX-License-Identifier: LGPL-3.0-or-later @@ -11,11 +11,11 @@ import tarfile import tempfile -from . import __version__ #, schema, sigmf_hash, validate +from . import __version__ +from .archive import SIGMF_ARCHIVE_EXT, SIGMF_DATASET_EXT, SIGMF_METADATA_EXT, SigMFArchive +from .error import SigMFFileError from .sigmffile import SigMFFile -from .archive import SigMFArchive, SIGMF_DATASET_EXT, SIGMF_METADATA_EXT, SIGMF_ARCHIVE_EXT from .utils import dict_merge -from .error import SigMFFileError class SigMFArchiveReader(): diff --git a/sigmf/error.py b/sigmf/error.py index df4e2ae..92ac194 100644 --- a/sigmf/error.py +++ b/sigmf/error.py @@ -1,6 +1,6 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/sigmf/schema-collection.json b/sigmf/schema-collection.json index b4540b5..96b28f3 100644 --- a/sigmf/schema-collection.json +++ b/sigmf/schema-collection.json @@ -1,5 +1,5 @@ { - "$id": "https://github.com/gnuradio/SigMF", + "$id": "https://github.com/sigmf/SigMF", "$schema": "http://json-schema.org/draft-07/schema", "default": {}, "required": [ @@ -20,7 +20,7 @@ "$id": "#/properties/collection/properties/core%3Aversion", "description": "The version of the SigMF specification used to create the Collection file.", "examples": [ - "1.0.0" + "1.2.0" ], "type": "string" }, diff --git a/sigmf/schema-meta.json b/sigmf/schema-meta.json index c3b7ad9..9e83cf3 100644 --- a/sigmf/schema-meta.json +++ b/sigmf/schema-meta.json @@ -1,5 +1,5 @@ { - "$id": "https://github.com/gnuradio/SigMF", + "$id": "https://github.com/sigmf/SigMF", "$schema": "http://json-schema.org/draft-07/schema", "default": [ "global", diff --git a/sigmf/schema.py b/sigmf/schema.py index a1ca14f..a34c5d2 100644 --- a/sigmf/schema.py +++ b/sigmf/schema.py @@ -1,13 +1,13 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later '''Schema IO''' -import os import json +import os from . import utils diff --git a/sigmf/sigmf_hash.py b/sigmf/sigmf_hash.py index 5f07768..414dc36 100644 --- a/sigmf/sigmf_hash.py +++ b/sigmf/sigmf_hash.py @@ -1,6 +1,6 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/sigmf/sigmffile.py b/sigmf/sigmffile.py index 91a31b7..098d65d 100644 --- a/sigmf/sigmffile.py +++ b/sigmf/sigmffile.py @@ -1,24 +1,26 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later '''SigMFFile Object''' -from collections import OrderedDict import codecs import json import tarfile import tempfile -from os import path import warnings +from collections import OrderedDict +from os import path + import numpy as np -from . import __version__, schema, sigmf_hash, validate -from .archive import SigMFArchive, SIGMF_DATASET_EXT, SIGMF_METADATA_EXT, SIGMF_ARCHIVE_EXT, SIGMF_COLLECTION_EXT +from . import __specification__, __version__, schema, sigmf_hash, validate +from .archive import SIGMF_ARCHIVE_EXT, SIGMF_COLLECTION_EXT, SIGMF_DATASET_EXT, SIGMF_METADATA_EXT, SigMFArchive +from .error import SigMFAccessError, SigMFFileError from .utils import dict_merge -from .error import SigMFFileError, SigMFAccessError + class SigMFMetafile(): VALID_KEYS = {} @@ -174,7 +176,7 @@ def __init__(self, metadata=None, data_file=None, global_info=None, skip_checksu if metadata is None: self._metadata = {self.GLOBAL_KEY:{}, self.CAPTURE_KEY:[], self.ANNOTATION_KEY:[]} self._metadata[self.GLOBAL_KEY][self.NUM_CHANNELS_KEY] = 1 - self._metadata[self.GLOBAL_KEY][self.VERSION_KEY] = "1.0.0" + self._metadata[self.GLOBAL_KEY][self.VERSION_KEY] = __specification__ elif isinstance(metadata, dict): self._metadata = metadata else: diff --git a/sigmf/utils.py b/sigmf/utils.py index 18c7cf8..2e61b42 100644 --- a/sigmf/utils.py +++ b/sigmf/utils.py @@ -1,16 +1,17 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later """Utilities""" +import re +import sys from copy import deepcopy from datetime import datetime -import sys + import numpy as np -import re from . import error diff --git a/sigmf/validate.py b/sigmf/validate.py index ce18fcc..95f8900 100644 --- a/sigmf/validate.py +++ b/sigmf/validate.py @@ -1,14 +1,19 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later '''SigMF Validator''' +import argparse +import json +import logging + import jsonschema -from . import schema +from . import __version__ as toolversion +from . import error, schema, sigmffile def extend_with_default(validator_class): @@ -80,15 +85,6 @@ def validate(metadata, ref_schema=schema.get_schema()): def main(): - import argparse - import logging - import json - - from . import sigmffile - from . import error - - from sigmf import __version__ as toolversion - parser = argparse.ArgumentParser(description='Validate SigMF Archive or file pair against JSON schema.', prog='sigmf_validate') parser.add_argument('filename', help='SigMF path (extension optional).') diff --git a/tests/conftest.py b/tests/conftest.py index f8f6aa4..0e46aaf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,27 +1,16 @@ -# Copyright 2017 GNU Radio Foundation +# Copyright: Multiple Authors # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# SPDX-License-Identifier: LGPL-3.0-or-later + +"""Provides pytest fixtures for other tests.""" import tempfile import pytest +from sigmf import __specification__ from sigmf.sigmffile import SigMFFile from .testdata import TEST_FLOAT32_DATA, TEST_METADATA @@ -38,11 +27,11 @@ def test_data_file(): @pytest.fixture def test_sigmffile(test_data_file): """If pytest uses this signature, will return valid SigMF file.""" - sigf = SigMFFile() - sigf.set_global_field("core:datatype", "rf32_le") - sigf.set_global_field("core:version", "1.0.0") - sigf.add_annotation(start_index=0, length=len(TEST_FLOAT32_DATA)) - sigf.add_capture(start_index=0) - sigf.set_data_file(test_data_file.name) - assert sigf._metadata == TEST_METADATA - return sigf + meta = SigMFFile() + meta.set_global_field("core:datatype", "rf32_le") + meta.set_global_field("core:version", __specification__) + meta.add_annotation(start_index=0, length=len(TEST_FLOAT32_DATA)) + meta.add_capture(start_index=0) + meta.set_data_file(test_data_file.name) + assert meta._metadata == TEST_METADATA + return meta diff --git a/tests/test_archive.py b/tests/test_archive.py index 5c3d67b..eccb1b4 100644 --- a/tests/test_archive.py +++ b/tests/test_archive.py @@ -1,12 +1,20 @@ +# Copyright: Multiple Authors +# +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +"""Tests for SigMFArchive""" + import codecs import json import tarfile import tempfile from os import path +import jsonschema import numpy as np import pytest -import jsonschema from sigmf import error from sigmf.archive import SIGMF_DATASET_EXT, SIGMF_METADATA_EXT @@ -75,7 +83,7 @@ def test_unwritable_fileobj_throws_fileerror(test_sigmffile): def test_unwritable_name_throws_fileerror(test_sigmffile): # Cannot assume /root/ is unwritable (e.g. Docker environment) # so use invalid filename - unwritable_file = '/bad_name/' + unwritable_file = "/bad_name/" with pytest.raises(error.SigMFFileError): test_sigmffile.archive(name=unwritable_file) diff --git a/tests/test_archivereader.py b/tests/test_archivereader.py index 1818285..a6053f5 100644 --- a/tests/test_archivereader.py +++ b/tests/test_archivereader.py @@ -1,10 +1,17 @@ -# Copyright 2023 GNU Radio Foundation +# Copyright: Multiple Authors +# +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +"""Tests for SigMFArchiveReader""" + import tempfile -import numpy as np import unittest -import sigmf -from sigmf import SigMFFile, SigMFArchiveReader +import numpy as np + +from sigmf import SigMFArchiveReader, SigMFFile, __specification__ class TestArchiveReader(unittest.TestCase): @@ -42,7 +49,7 @@ def test_access_data_without_untar(self): global_info={ SigMFFile.DATATYPE_KEY: f"{complex_prefix}{key}_le", SigMFFile.NUM_CHANNELS_KEY: num_channels, - SigMFFile.VERSION_KEY: "1.0.0", + SigMFFile.VERSION_KEY: __specification__, }, ) temp_meta.tofile(temp_archive, toarchive=True) diff --git a/tests/test_sigmffile.py b/tests/test_sigmffile.py index 55672a3..54c3be6 100644 --- a/tests/test_sigmffile.py +++ b/tests/test_sigmffile.py @@ -1,31 +1,20 @@ -# Copyright 2017 GNU Radio Foundation +# Copyright: Multiple Authors # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# SPDX-License-Identifier: LGPL-3.0-or-later + +"""Tests for SigMFFile Object""" +import copy +import json import os import shutil import tempfile -import json +import unittest from pathlib import Path + import numpy as np -import unittest -import copy from sigmf import sigmffile, utils from sigmf.sigmffile import SigMFFile @@ -62,8 +51,8 @@ def test_iterator_basic(self): count += 1 self.assertEqual(count, len(self.sigmf_object)) -class TestAnnotationHandling(unittest.TestCase): +class TestAnnotationHandling(unittest.TestCase): def test_get_annotations_with_index(self): """Test that only annotations containing index are returned from get_annotations()""" smf = SigMFFile(copy.deepcopy(TEST_METADATA)) @@ -77,7 +66,7 @@ def test_get_annotations_with_index(self): {SigMFFile.START_INDEX_KEY: 1}, ] ) - + def test__count_samples_from_annotation(self): """Make sure sample count from annotations use correct end index""" smf = SigMFFile(copy.deepcopy(TEST_METADATA)) @@ -85,7 +74,7 @@ def test__count_samples_from_annotation(self): smf.add_annotation(start_index=4, length=4) sample_count = smf._count_samples() self.assertEqual(sample_count, 32) - + def test_set_data_file_without_annotations(self): """ Make sure setting data_file with no annotations registered does not @@ -98,7 +87,7 @@ def test_set_data_file_without_annotations(self): TEST_FLOAT32_DATA.tofile(temp_path_data) smf.set_data_file(temp_path_data) samples = smf.read_samples() - self.assertTrue(len(samples)==16) + self.assertTrue(len(samples) == 16) def test_set_data_file_with_annotations(self): """ @@ -115,7 +104,8 @@ def test_set_data_file_with_annotations(self): # Issues warning since file ends before the final annotatio smf.set_data_file(temp_path_data) samples = smf.read_samples() - self.assertTrue(len(samples)==16) + self.assertTrue(len(samples) == 16) + def simulate_capture(sigmf_md, n, capture_len): start_index = capture_len * n @@ -129,9 +119,7 @@ def simulate_capture(sigmf_md, n, capture_len): "core:longitude": -105.0 + 0.0001 * n, } - sigmf_md.add_annotation(start_index=start_index, - length=capture_len, - metadata=annotation_md) + sigmf_md.add_annotation(start_index=start_index, length=capture_len, metadata=annotation_md) def test_default_constructor(): @@ -140,7 +128,7 @@ def test_default_constructor(): def test_set_non_required_global_field(): sigf = SigMFFile() - sigf.set_global_field('this_is:not_in_the_schema', None) + sigf.set_global_field("this_is:not_in_the_schema", None) def test_add_capture(): @@ -237,89 +225,95 @@ def test_multichannel_seek(self): def test_key_validity(): - '''assure the keys in test metadata are valid''' + """assure the keys in test metadata are valid""" for top_key, top_val in TEST_METADATA.items(): if type(top_val) is dict: for core_key in top_val.keys(): - assert core_key in vars(SigMFFile)[f'VALID_{top_key.upper()}_KEYS'] + assert core_key in vars(SigMFFile)[f"VALID_{top_key.upper()}_KEYS"] elif type(top_val) is list: # annotations are in a list for annot in top_val: for core_key in annot.keys(): assert core_key in SigMFFile.VALID_ANNOTATION_KEYS else: - raise ValueError('expected list or dict') + raise ValueError("expected list or dict") def test_ordered_metadata(): - '''check to make sure the metadata is sorted as expected''' + """check to make sure the metadata is sorted as expected""" sigf = SigMFFile() - top_sort_order = ['global', 'captures', 'annotations'] + top_sort_order = ["global", "captures", "annotations"] for kdx, key in enumerate(sigf.ordered_metadata()): assert kdx == top_sort_order.index(key) def test_captures_checking(): - ''' + """ these tests make sure the various captures access tools work properly - ''' - np.array(TEST_U8_DATA0, dtype=np.uint8).tofile('/tmp/d0.sigmf-data') - with open('/tmp/d0.sigmf-meta','w') as f0: json.dump(TEST_U8_META0, f0) - np.array(TEST_U8_DATA1, dtype=np.uint8).tofile('/tmp/d1.sigmf-data') - with open('/tmp/d1.sigmf-meta','w') as f1: json.dump(TEST_U8_META1, f1) - np.array(TEST_U8_DATA2, dtype=np.uint8).tofile('/tmp/d2.sigmf-data') - with open('/tmp/d2.sigmf-meta','w') as f2: json.dump(TEST_U8_META2, f2) - np.array(TEST_U8_DATA3, dtype=np.uint8).tofile('/tmp/d3.sigmf-data') - with open('/tmp/d3.sigmf-meta','w') as f3: json.dump(TEST_U8_META3, f3) - np.array(TEST_U8_DATA4, dtype=np.uint8).tofile('/tmp/d4.sigmf-data') - with open('/tmp/d4.sigmf-meta','w') as f4: json.dump(TEST_U8_META4, f4) - - sigmf0 = sigmffile.fromfile('/tmp/d0.sigmf-meta', skip_checksum=True) - sigmf1 = sigmffile.fromfile('/tmp/d1.sigmf-meta', skip_checksum=True) - sigmf2 = sigmffile.fromfile('/tmp/d2.sigmf-meta', skip_checksum=True) - sigmf3 = sigmffile.fromfile('/tmp/d3.sigmf-meta', skip_checksum=True) - sigmf4 = sigmffile.fromfile('/tmp/d4.sigmf-meta', skip_checksum=True) + """ + np.array(TEST_U8_DATA0, dtype=np.uint8).tofile("/tmp/d0.sigmf-data") + with open("/tmp/d0.sigmf-meta", "w") as f0: + json.dump(TEST_U8_META0, f0) + np.array(TEST_U8_DATA1, dtype=np.uint8).tofile("/tmp/d1.sigmf-data") + with open("/tmp/d1.sigmf-meta", "w") as f1: + json.dump(TEST_U8_META1, f1) + np.array(TEST_U8_DATA2, dtype=np.uint8).tofile("/tmp/d2.sigmf-data") + with open("/tmp/d2.sigmf-meta", "w") as f2: + json.dump(TEST_U8_META2, f2) + np.array(TEST_U8_DATA3, dtype=np.uint8).tofile("/tmp/d3.sigmf-data") + with open("/tmp/d3.sigmf-meta", "w") as f3: + json.dump(TEST_U8_META3, f3) + np.array(TEST_U8_DATA4, dtype=np.uint8).tofile("/tmp/d4.sigmf-data") + with open("/tmp/d4.sigmf-meta", "w") as f4: + json.dump(TEST_U8_META4, f4) + + sigmf0 = sigmffile.fromfile("/tmp/d0.sigmf-meta", skip_checksum=True) + sigmf1 = sigmffile.fromfile("/tmp/d1.sigmf-meta", skip_checksum=True) + sigmf2 = sigmffile.fromfile("/tmp/d2.sigmf-meta", skip_checksum=True) + sigmf3 = sigmffile.fromfile("/tmp/d3.sigmf-meta", skip_checksum=True) + sigmf4 = sigmffile.fromfile("/tmp/d4.sigmf-meta", skip_checksum=True) assert sigmf0._count_samples() == 256 assert sigmf0._is_conforming_dataset() - assert (0,0) == sigmf0.get_capture_byte_boundarys(0) - assert (0,256) == sigmf0.get_capture_byte_boundarys(1) + assert (0, 0) == sigmf0.get_capture_byte_boundarys(0) + assert (0, 256) == sigmf0.get_capture_byte_boundarys(1) assert np.array_equal(TEST_U8_DATA0, sigmf0.read_samples(autoscale=False)) assert np.array_equal(np.array([]), sigmf0.read_samples_in_capture(0)) - assert np.array_equal(TEST_U8_DATA0, sigmf0.read_samples_in_capture(1,autoscale=False)) + assert np.array_equal(TEST_U8_DATA0, sigmf0.read_samples_in_capture(1, autoscale=False)) assert sigmf1._count_samples() == 192 assert not sigmf1._is_conforming_dataset() - assert (32,160) == sigmf1.get_capture_byte_boundarys(0) - assert (160,224) == sigmf1.get_capture_byte_boundarys(1) - assert np.array_equal(np.array(range(128)), sigmf1.read_samples_in_capture(0,autoscale=False)) - assert np.array_equal(np.array(range(128,192)), sigmf1.read_samples_in_capture(1,autoscale=False)) + assert (32, 160) == sigmf1.get_capture_byte_boundarys(0) + assert (160, 224) == sigmf1.get_capture_byte_boundarys(1) + assert np.array_equal(np.array(range(128)), sigmf1.read_samples_in_capture(0, autoscale=False)) + assert np.array_equal(np.array(range(128, 192)), sigmf1.read_samples_in_capture(1, autoscale=False)) assert sigmf2._count_samples() == 192 assert not sigmf2._is_conforming_dataset() - assert (32,160) == sigmf2.get_capture_byte_boundarys(0) - assert (176,240) == sigmf2.get_capture_byte_boundarys(1) - assert np.array_equal(np.array(range(128)), sigmf2.read_samples_in_capture(0,autoscale=False)) - assert np.array_equal(np.array(range(128,192)), sigmf2.read_samples_in_capture(1,autoscale=False)) + assert (32, 160) == sigmf2.get_capture_byte_boundarys(0) + assert (176, 240) == sigmf2.get_capture_byte_boundarys(1) + assert np.array_equal(np.array(range(128)), sigmf2.read_samples_in_capture(0, autoscale=False)) + assert np.array_equal(np.array(range(128, 192)), sigmf2.read_samples_in_capture(1, autoscale=False)) assert sigmf3._count_samples() == 192 assert not sigmf3._is_conforming_dataset() - assert (32,64) == sigmf3.get_capture_byte_boundarys(0) - assert (64,160) == sigmf3.get_capture_byte_boundarys(1) - assert (192,256) == sigmf3.get_capture_byte_boundarys(2) - assert np.array_equal(np.array(range(32)), sigmf3.read_samples_in_capture(0,autoscale=False)) - assert np.array_equal(np.array(range(32,128)), sigmf3.read_samples_in_capture(1,autoscale=False)) - assert np.array_equal(np.array(range(128,192)), sigmf3.read_samples_in_capture(2,autoscale=False)) + assert (32, 64) == sigmf3.get_capture_byte_boundarys(0) + assert (64, 160) == sigmf3.get_capture_byte_boundarys(1) + assert (192, 256) == sigmf3.get_capture_byte_boundarys(2) + assert np.array_equal(np.array(range(32)), sigmf3.read_samples_in_capture(0, autoscale=False)) + assert np.array_equal(np.array(range(32, 128)), sigmf3.read_samples_in_capture(1, autoscale=False)) + assert np.array_equal(np.array(range(128, 192)), sigmf3.read_samples_in_capture(2, autoscale=False)) assert sigmf4._count_samples() == 96 assert not sigmf4._is_conforming_dataset() - assert (32,160) == sigmf4.get_capture_byte_boundarys(0) - assert (160,224) == sigmf4.get_capture_byte_boundarys(1) - assert np.array_equal(np.array(range(64)), sigmf4.read_samples_in_capture(0,autoscale=False)[:,0]) - assert np.array_equal(np.array(range(64,96)), sigmf4.read_samples_in_capture(1,autoscale=False)[:,1]) + assert (32, 160) == sigmf4.get_capture_byte_boundarys(0) + assert (160, 224) == sigmf4.get_capture_byte_boundarys(1) + assert np.array_equal(np.array(range(64)), sigmf4.read_samples_in_capture(0, autoscale=False)[:, 0]) + assert np.array_equal(np.array(range(64, 96)), sigmf4.read_samples_in_capture(1, autoscale=False)[:, 1]) + def test_slicing(): - '''Test __getitem___ builtin for sigmffile, make sure slicing and indexing works as expected.''' + """Test __getitem___ builtin for sigmffile, make sure slicing and indexing works as expected.""" _, temp_data0 = tempfile.mkstemp() np.array(TEST_U8_DATA0, dtype=np.uint8).tofile(temp_data0) sigmf0 = SigMFFile(metadata=TEST_U8_META0, data_file=temp_data0) @@ -337,8 +331,8 @@ def test_slicing(): _, temp_data2 = tempfile.mkstemp() np.array(TEST_U8_DATA4, dtype=np.uint8).tofile(temp_data2) sigmf2 = SigMFFile(TEST_U8_META4, data_file=temp_data2) - channelized = np.array(TEST_U8_DATA4).reshape((128,2)) + channelized = np.array(TEST_U8_DATA4).reshape((128, 2)) assert np.array_equal(channelized, sigmf2[:][:]) assert np.array_equal(sigmf2[10:20, 91:112], sigmf2.read_samples(autoscale=False)[10:20, 91:112]) assert np.array_equal(sigmf2[0], channelized[0]) - assert np.array_equal(sigmf2[1,:], channelized[1,:]) + assert np.array_equal(sigmf2[1, :], channelized[1, :]) diff --git a/tests/test_utils.py b/tests/test_utils.py index 9a98ed4..e3599dc 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,3 +1,11 @@ +# Copyright: Multiple Authors +# +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +"""Tests for Utilities""" + from datetime import datetime import pytest diff --git a/tests/test_validation.py b/tests/test_validation.py index ce427a1..7b74be9 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -1,79 +1,66 @@ -# Copyright 2016 GNU Radio Foundation +# Copyright: Multiple Authors # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# SPDX-License-Identifier: LGPL-3.0-or-later + +"""Tests for Validator""" import tempfile import unittest +from jsonschema.exceptions import ValidationError + import sigmf from sigmf import SigMFFile -from jsonschema.exceptions import ValidationError - from .testdata import TEST_FLOAT32_DATA, TEST_METADATA def test_valid_data(): - '''assure the supplied metadata is OK''' + """assure the supplied metadata is OK""" invalid_metadata = dict(TEST_METADATA) SigMFFile(TEST_METADATA).validate() + class FailingCases(unittest.TestCase): - '''Cases where the validator should throw an exception.''' + """Cases where the validator should throw an exception.""" + def setUp(self): self.metadata = dict(TEST_METADATA) def test_no_version(self): - '''core:version must be present''' + """core:version must be present""" del self.metadata[SigMFFile.GLOBAL_KEY][SigMFFile.VERSION_KEY] with self.assertRaises(ValidationError): SigMFFile(self.metadata).validate() def test_extra_top_level_key(self): - '''no extra keys allowed on the top level''' - self.metadata['extra'] = 0 + """no extra keys allowed on the top level""" + self.metadata["extra"] = 0 with self.assertRaises(ValidationError): SigMFFile(self.metadata).validate() def test_extra_top_level_key(self): - '''label must be less than 20 chars''' - self.metadata[SigMFFile.ANNOTATION_KEY][0][SigMFFile.LABEL_KEY] = 'a' * 21 + """label must be less than 20 chars""" + self.metadata[SigMFFile.ANNOTATION_KEY][0][SigMFFile.LABEL_KEY] = "a" * 21 with self.assertRaises(ValidationError): SigMFFile(self.metadata).validate() def test_invalid_type(self): - '''license key must be string''' + """license key must be string""" self.metadata[SigMFFile.GLOBAL_KEY][SigMFFile.LICENSE_KEY] = 1 with self.assertRaises(ValidationError): SigMFFile(self.metadata).validate() def test_invalid_capture_order(self): - '''metadata must have captures in order''' - self.metadata[SigMFFile.CAPTURE_KEY] = [ - {SigMFFile.START_INDEX_KEY: 10}, - {SigMFFile.START_INDEX_KEY: 9} - ] + """metadata must have captures in order""" + self.metadata[SigMFFile.CAPTURE_KEY] = [{SigMFFile.START_INDEX_KEY: 10}, {SigMFFile.START_INDEX_KEY: 9}] with self.assertRaises(ValidationError): SigMFFile(self.metadata).validate() def test_invalid_annotation_order(self): - '''metadata must have annotations in order''' + """metadata must have annotations in order""" self.metadata[SigMFFile.ANNOTATION_KEY] = [ { SigMFFile.START_INDEX_KEY: 2, @@ -82,24 +69,19 @@ def test_invalid_annotation_order(self): { SigMFFile.START_INDEX_KEY: 1, SigMFFile.LENGTH_INDEX_KEY: 120000, - } + }, ] with self.assertRaises(ValidationError): SigMFFile(self.metadata).validate() def test_annotation_without_sample_count(self): - '''annotation without length should be accepted''' - self.metadata[SigMFFile.ANNOTATION_KEY] = [ - { - SigMFFile.START_INDEX_KEY: 2 - } - ] + """annotation without length should be accepted""" + self.metadata[SigMFFile.ANNOTATION_KEY] = [{SigMFFile.START_INDEX_KEY: 2}] SigMFFile(self.metadata).validate() - def test_invalid_hash(self): _, temp_path = tempfile.mkstemp() TEST_FLOAT32_DATA.tofile(temp_path) - self.metadata[SigMFFile.GLOBAL_KEY][SigMFFile.HASH_KEY] = 'derp' + self.metadata[SigMFFile.GLOBAL_KEY][SigMFFile.HASH_KEY] = "derp" with self.assertRaises(sigmf.error.SigMFFileError): SigMFFile(metadata=self.metadata, data_file=temp_path) diff --git a/tests/testdata.py b/tests/testdata.py index cb05bee..b91ad67 100644 --- a/tests/testdata.py +++ b/tests/testdata.py @@ -1,29 +1,14 @@ -# flake8: noqa - -# Copyright 2017 GNU Radio Foundation -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright: Multiple Authors # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Shared test data for tests.""" import numpy as np -from sigmf import __version__ -from sigmf import SigMFFile + +from sigmf import SigMFFile, __specification__, __version__ TEST_FLOAT32_DATA = np.arange(16, dtype=np.float32) @@ -31,51 +16,65 @@ SigMFFile.ANNOTATION_KEY: [{SigMFFile.LENGTH_INDEX_KEY: 16, SigMFFile.START_INDEX_KEY: 0}], SigMFFile.CAPTURE_KEY: [{SigMFFile.START_INDEX_KEY: 0}], SigMFFile.GLOBAL_KEY: { - SigMFFile.DATATYPE_KEY: 'rf32_le', - SigMFFile.HASH_KEY: 'f4984219b318894fa7144519185d1ae81ea721c6113243a52b51e444512a39d74cf41a4cec3c5d000bd7277cc71232c04d7a946717497e18619bdbe94bfeadd6', + SigMFFile.DATATYPE_KEY: "rf32_le", + SigMFFile.HASH_KEY: "f4984219b318894fa7144519185d1ae81ea721c6113243a52b51e444512a39d74cf41a4cec3c5d000bd7277cc71232c04d7a946717497e18619bdbe94bfeadd6", SigMFFile.NUM_CHANNELS_KEY: 1, - SigMFFile.VERSION_KEY: '1.0.0' - } + SigMFFile.VERSION_KEY: __specification__, + }, } # Data0 is a test of a compliant two capture recording TEST_U8_DATA0 = list(range(256)) TEST_U8_META0 = { SigMFFile.ANNOTATION_KEY: [], - SigMFFile.CAPTURE_KEY: [ {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 0}, - {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 0} ], # very strange..but technically legal? - SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: 'ru8', SigMFFile.TRAILING_BYTES_KEY: 0} + SigMFFile.CAPTURE_KEY: [ + {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 0}, + {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 0}, + ], # very strange..but technically legal? + SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: "ru8", SigMFFile.TRAILING_BYTES_KEY: 0}, } # Data1 is a test of a two capture recording with header_bytes and trailing_bytes set -TEST_U8_DATA1 = [0xfe]*32 + list(range(192)) + [0xff]*32 +TEST_U8_DATA1 = [0xFE] * 32 + list(range(192)) + [0xFF] * 32 TEST_U8_META1 = { SigMFFile.ANNOTATION_KEY: [], - SigMFFile.CAPTURE_KEY: [ {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, - {SigMFFile.START_INDEX_KEY: 128} ], - SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: 'ru8', SigMFFile.TRAILING_BYTES_KEY: 32} + SigMFFile.CAPTURE_KEY: [ + {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, + {SigMFFile.START_INDEX_KEY: 128}, + ], + SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: "ru8", SigMFFile.TRAILING_BYTES_KEY: 32}, } # Data2 is a test of a two capture recording with multiple header_bytes set -TEST_U8_DATA2 = [0xfe]*32 + list(range(128)) + [0xfe]*16 + list(range(128,192)) + [0xff]*16 +TEST_U8_DATA2 = [0xFE] * 32 + list(range(128)) + [0xFE] * 16 + list(range(128, 192)) + [0xFF] * 16 TEST_U8_META2 = { SigMFFile.ANNOTATION_KEY: [], - SigMFFile.CAPTURE_KEY: [ {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, - {SigMFFile.START_INDEX_KEY: 128, SigMFFile.HEADER_BYTES_KEY: 16} ], - SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: 'ru8', SigMFFile.TRAILING_BYTES_KEY: 16} + SigMFFile.CAPTURE_KEY: [ + {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, + {SigMFFile.START_INDEX_KEY: 128, SigMFFile.HEADER_BYTES_KEY: 16}, + ], + SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: "ru8", SigMFFile.TRAILING_BYTES_KEY: 16}, } # Data3 is a test of a three capture recording with multiple header_bytes set -TEST_U8_DATA3 = [0xfe]*32 + list(range(128)) + [0xfe]*32 + list(range(128,192)) +TEST_U8_DATA3 = [0xFE] * 32 + list(range(128)) + [0xFE] * 32 + list(range(128, 192)) TEST_U8_META3 = { SigMFFile.ANNOTATION_KEY: [], - SigMFFile.CAPTURE_KEY: [ {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, - {SigMFFile.START_INDEX_KEY: 32}, - {SigMFFile.START_INDEX_KEY: 128, SigMFFile.HEADER_BYTES_KEY: 32} ], - SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: 'ru8'} + SigMFFile.CAPTURE_KEY: [ + {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, + {SigMFFile.START_INDEX_KEY: 32}, + {SigMFFile.START_INDEX_KEY: 128, SigMFFile.HEADER_BYTES_KEY: 32}, + ], + SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: "ru8"}, } # Data4 is a two channel version of Data0 -TEST_U8_DATA4 = [0xfe]*32 + [y for y in list(range(96)) for i in [0,1]] + [0xff]*32 +TEST_U8_DATA4 = [0xFE] * 32 + [y for y in list(range(96)) for i in [0, 1]] + [0xFF] * 32 TEST_U8_META4 = { SigMFFile.ANNOTATION_KEY: [], - SigMFFile.CAPTURE_KEY: [ {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, - {SigMFFile.START_INDEX_KEY: 64} ], - SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: 'ru8', SigMFFile.TRAILING_BYTES_KEY: 32, SigMFFile.NUM_CHANNELS_KEY: 2} + SigMFFile.CAPTURE_KEY: [ + {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, + {SigMFFile.START_INDEX_KEY: 64}, + ], + SigMFFile.GLOBAL_KEY: { + SigMFFile.DATATYPE_KEY: "ru8", + SigMFFile.TRAILING_BYTES_KEY: 32, + SigMFFile.NUM_CHANNELS_KEY: 2, + }, }