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 d423975..7d9f23d 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. +is located in the [SigMF](https://github.com/sigmf/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..15422cf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,104 @@ +[project] +name = "SigMF" +description = "Easily interact with Signal Metadata Format (SigMF) recordings." +keywords = ["gnuradio", "radio"] +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", + "Topic :: Scientific/Engineering", + "Topic :: Communications :: Ham Radio", +] +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..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.1.5" +# 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/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..9151d2f --- /dev/null +++ b/sigmf/apps/convert_wav.py @@ -0,0 +1,93 @@ +# Copyright: Multiple Authors +# +# 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 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 ..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 = { + 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, + SigMFFile.VERSION_KEY: __specification__, + } + + if start_datetime is None: + mtime = datetime.datetime.fromtimestamp(input_path.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_stem + 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 = input_stem + 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") + 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, + ) + log.info(f"Write {out_fname}") + + +if __name__ == "__main__": + main() diff --git a/sigmf/gui.py b/sigmf/apps/gui.py similarity index 98% rename from sigmf/gui.py rename to sigmf/apps/gui.py index 2814f3e..dc6f74f 100644 --- a/sigmf/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 .archive import SIGMF_ARCHIVE_EXT +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 e1be40f..29a0f56 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 @@ -17,7 +17,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 b61d9d5..4b0522c 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 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 c6ca130..1ef5702 100644 --- a/sigmf/sigmffile.py +++ b/sigmf/sigmffile.py @@ -1,23 +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 from os import path import warnings +from collections import OrderedDict +from os import path + import numpy as np -from . import schema, sigmf_hash, validate -from .archive import SigMFArchive, SIGMF_DATASET_EXT, SIGMF_METADATA_EXT, SIGMF_ARCHIVE_EXT, SIGMF_COLLECTION_EXT +from . import __specification__, 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 .sigmffile_collection import AbstractSigMFFileCollection, SigMFFileCollection from .utils import dict_merge -from .error import SigMFFileError, SigMFAccessError + class SigMFMetafile(): VALID_KEYS = {} @@ -197,6 +200,7 @@ def __init__(self, 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] = __specification__ elif isinstance(metadata, dict): self._metadata = metadata else: @@ -205,9 +209,6 @@ def __init__(self, self.set_global_info(global_info) if data_file is not None: self.set_data_file(data_file, skip_checksum=skip_checksum, map_readonly=map_readonly) - self.name = name - - self._metadata[self.GLOBAL_KEY][self.VERSION_KEY] = '1.0.0' def __len__(self): return self._memmap.shape[0] @@ -226,7 +227,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 @@ -267,13 +268,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 @@ -443,7 +446,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]: @@ -454,7 +457,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 @@ -786,8 +789,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/sigmf/tools/wav2sigmf.py b/sigmf/tools/wav2sigmf.py deleted file mode 100755 index fae3f0d..0000000 --- a/sigmf/tools/wav2sigmf.py +++ /dev/null @@ -1,58 +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(name=os.path.basename(input_wav_filename), - 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/utils.py b/sigmf/utils.py index 5f54e1f..2e61b42 100644 --- a/sigmf/utils.py +++ b/sigmf/utils.py @@ -1,14 +1,16 @@ # 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 from . import error @@ -30,6 +32,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/sigmf/validate.py b/sigmf/validate.py index a251bdd..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).') @@ -124,5 +120,5 @@ def main(): log.info('Validation OK!') -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/tests/conftest.py b/tests/conftest.py index 9f5f71f..d2edcd8 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_1, @@ -83,10 +72,11 @@ def test_alternate_sigmffile(test_data_file_2): @pytest.fixture def test_alternate_sigmffile_2(test_data_file_3): """If pytest uses this signature, will return valid SigMF file.""" - f = SigMFFile(name='test3') - f.set_global_field("core:datatype", "rf32_le") - f.add_annotation(start_index=0, length=len(TEST_FLOAT32_DATA_3)) - f.add_capture(start_index=0) - f.set_data_file(test_data_file_3.name) - assert f._metadata == TEST_METADATA_3 - return f + 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_3)) + meta.add_capture(start_index=0) + meta.set_data_file(test_data_file_3.name) + assert meta._metadata == TEST_METADATA_3 + return meta diff --git a/tests/test_archive.py b/tests/test_archive.py index 4ef0936..46fc9cc 100644 --- a/tests/test_archive.py +++ b/tests/test_archive.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 SigMFArchive""" + import codecs import json import os @@ -6,9 +14,9 @@ import tempfile from os import path +import jsonschema import numpy as np import pytest -import jsonschema from sigmf import error, sigmffile from sigmf.archive import (SIGMF_DATASET_EXT, @@ -83,7 +91,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 2cd867b..c55c27e 100644 --- a/tests/test_archivereader.py +++ b/tests/test_archivereader.py @@ -1,14 +1,17 @@ -# Copyright 2023 GNU Radio Foundation -import os -import shutil +# 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 unittest import numpy as np -import unittest -from sigmf import SigMFFile, SigMFArchiveReader -from sigmf.archive import SIGMF_METADATA_EXT, SigMFArchive -from sigmf.sigmffile_collection import SigMFFileCollection +from sigmf import SigMFArchiveReader, SigMFFile, __specification__ class TestArchiveReader(unittest.TestCase): @@ -47,6 +50,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: __specification__, }, ) temp_meta.tofile(temp_archive, toarchive=True) diff --git a/tests/test_sigmffile.py b/tests/test_sigmffile.py index bb82cfd..ee0ee3d 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, fromarchive @@ -64,8 +53,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("test", copy.deepcopy(TEST_METADATA_1)) @@ -79,7 +68,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("test", copy.deepcopy(TEST_METADATA_1)) @@ -87,7 +76,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 @@ -100,7 +89,7 @@ def test_set_data_file_without_annotations(self): TEST_FLOAT32_DATA_1.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): """ @@ -117,7 +106,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 @@ -131,9 +121,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(): @@ -296,14 +284,14 @@ def test_key_validity(): for top_key, top_val in TEST_METADATA_1.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(): @@ -315,66 +303,72 @@ def test_ordered_metadata(): 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("test1", metadata=TEST_U8_META0, data_file=temp_data0) @@ -396,4 +390,4 @@ def test_slicing(): 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 new file mode 100644 index 0000000..e3599dc --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,27 @@ +# 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 + +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 diff --git a/tests/test_validation.py b/tests/test_validation.py index 7302a79..8af130c 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -1,48 +1,46 @@ -# 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_1, TEST_METADATA_1 def test_valid_data(): - '''assure the supplied metadata is OK''' + """assure the supplied metadata is OK""" invalid_metadata = dict(TEST_METADATA_1) SigMFFile("test", TEST_METADATA_1).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_1) + 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 + """no extra keys allowed on the top level""" + self.metadata["extra"] = 0 with self.assertRaises(ValidationError): SigMFFile("test", self.metadata).validate() @@ -53,22 +51,19 @@ def test_invalid_label(self): SigMFFile("test", 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("test", 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("test", 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, @@ -77,24 +72,19 @@ def test_invalid_annotation_order(self): { SigMFFile.START_INDEX_KEY: 1, SigMFFile.LENGTH_INDEX_KEY: 120000, - } + }, ] with self.assertRaises(ValidationError): SigMFFile("test", 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("test", self.metadata).validate() - def test_invalid_hash(self): _, temp_path = tempfile.mkstemp() TEST_FLOAT32_DATA_1.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(name="test", metadata=self.metadata, data_file=temp_path) diff --git a/tests/testdata.py b/tests/testdata.py index 1d5684b..98c627a 100644 --- a/tests/testdata.py +++ b/tests/testdata.py @@ -1,28 +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 SigMFFile + +from sigmf import SigMFFile, __specification__, __version__ TEST_FLOAT32_DATA_1 = np.arange(16, dtype=np.float32) @@ -30,11 +16,11 @@ 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__, + }, } TEST_FLOAT32_DATA_2 = np.arange(16, 32, dtype=np.float32) @@ -67,40 +53,54 @@ 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, + }, } 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