Skip to content

Commit c5d194d

Browse files
authored
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
1 parent 1386965 commit c5d194d

14 files changed

+214
-152
lines changed

.github/workflows/main.yml

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: Python package
22

3-
on:
3+
on:
44
push:
55
pull_request:
66
types: [opened, synchronize]
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ubuntu-20.04
1111
strategy:
1212
matrix:
13-
python-version: ["3.6"]
13+
python-version: ["3.7", "3.9", "3.12"]
1414
steps:
1515
- uses: actions/checkout@v3
1616
- name: Set up Python ${{ matrix.python-version }}
@@ -20,8 +20,7 @@ jobs:
2020
- name: Install dependencies
2121
run: |
2222
python -m pip install --upgrade pip
23-
pip install pytest
24-
pip install .
23+
pip install .[test,apps]
2524
- name: Test with pytest
2625
run: |
27-
pytest
26+
coverage run

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ build/*
99
.eggs/*
1010
SigMF.egg-info/*
1111

12-
# pytest & coverage related
12+
# test related
1313
.coverage
1414
pytest.xml
1515
coverage.xml
16+
.tox/
1617
htmlcov/*

README.md

+17-10
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,41 @@
1-
<p align="center"><img src="https://github.com/sigmf/SigMF/raw/sigmf-v1.x/logo/sigmf_logo.png" width="30%" /></p>
1+
<p align="center"><img src="https://github.com/gnuradio/SigMF/blob/sigmf-v1.x/logo/sigmf_logo.svg" alt="Rendered SigMF Logo"/></p>
22

33
This python module makes it easy to interact with Signal Metadata Format
4-
(SigMF) objects. This module works with Python 3.6+ and is distributed freely
5-
under the terms GNU Lesser GPL v3 License.
4+
(SigMF) recordings. This module works with Python 3.7+ and is distributed
5+
freely under the terms GNU Lesser GPL v3 License.
66

77
The [SigMF specification document](https://github.com/sigmf/SigMF/blob/HEAD/sigmf-spec.md)
88
is located in the [SigMF](https://github.com/gnuradio/SigMF) repository.
99

1010
# Installation
1111

12-
To install the latest release, install from pip:
12+
To install the latest PyPi release, install from pip:
1313

1414
```bash
1515
pip install sigmf
1616
```
1717

18-
To install the latest development version, build from source:
18+
To install the latest git release, build from source:
1919

2020
```bash
2121
git clone https://github.com/sigmf/sigmf-python.git
2222
cd sigmf-python
2323
pip install .
2424
```
2525

26-
To run the included QA tests:
26+
Testing can be run with a variety of tools:
27+
2728
```bash
28-
# basic
29-
python3 -m pytest tests/
30-
# fancy
31-
coverage run --a --source sigmf -m pytest --doctest-modules
29+
# pytest and coverage run locally
30+
pytest
31+
coverage run
32+
# run coverage in a venv
33+
tox run
34+
# other useful tools
35+
pylint sigmf tests
36+
pytype
37+
black
38+
flake8
3239
```
3340

3441
# Examples

pyproject.toml

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
[project]
2+
name = "SigMF"
3+
description = "Easily interact with Signal Metadata Format (SigMF) recordings."
4+
keywords = ["gnuradio"]
5+
classifiers = [
6+
"Development Status :: 5 - Production/Stable",
7+
"License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
8+
"Operating System :: OS Independent",
9+
"Programming Language :: Python :: 3",
10+
"Programming Language :: Python :: 3.7",
11+
"Programming Language :: Python :: 3.8",
12+
"Programming Language :: Python :: 3.9",
13+
"Programming Language :: Python :: 3.10",
14+
"Programming Language :: Python :: 3.11",
15+
"Programming Language :: Python :: 3.12",
16+
]
17+
dynamic = ["version", "readme"]
18+
requires-python = ">=3.7"
19+
dependencies = [
20+
"numpy", # for vector math
21+
"jsonschema", # for spec validation
22+
]
23+
[project.urls]
24+
repository = "https://github.com/sigmf/sigmf-python"
25+
[project.scripts]
26+
sigmf_validate = "sigmf.validate:main"
27+
sigmf_gui = "sigmf.apps.gui:main [apps]"
28+
sigmf_convert_wav = "sigmf.apps.convert_wav:main [apps]"
29+
[project.optional-dependencies]
30+
test = [
31+
"pylint",
32+
"pytest",
33+
"pytest-cov",
34+
"hypothesis", # next-gen testing framework
35+
]
36+
apps = [
37+
"scipy", # for wav i/o
38+
# FIXME: PySimpleGUI 2024-02-12 v5.0.0 release seems to have a bug. Unpin version when possible.
39+
"PySimpleGUI < 5.0.0", # for gui interface
40+
]
41+
42+
[tool.setuptools]
43+
packages = ["sigmf"]
44+
[tool.setuptools.dynamic]
45+
version = {attr = "sigmf.__version__"}
46+
readme = {file = ["README.md"], content-type = "text/markdown"}
47+
[tool.setuptools.package-data]
48+
sigmf = ["*.json"]
49+
50+
[build-system]
51+
requires = ["setuptools>=65.0", "setuptools-scm"]
52+
build-backend = "setuptools.build_meta"
53+
54+
[tool.coverage.run]
55+
branch = true
56+
source = ["sigmf", "tests"]
57+
# -rA captures stdout from all tests and places it after the pytest summary
58+
command_line = "-m pytest -rA --doctest-modules --junitxml=pytest.xml"
59+
60+
[tool.pytest.ini_options]
61+
addopts = "--doctest-modules"
62+
63+
[tool.pylint]
64+
[tool.pylint.main]
65+
load-plugins = [
66+
"pylint.extensions.typing",
67+
"pylint.extensions.docparams",
68+
]
69+
exit-zero = true
70+
[tool.pylint.messages_control]
71+
disable = [
72+
"logging-not-lazy",
73+
"missing-module-docstring",
74+
"import-error",
75+
"unspecified-encoding",
76+
]
77+
max-line-length = 120
78+
[tool.pylint.REPORTS]
79+
# omit from the similarity reports
80+
ignore-comments = 'yes'
81+
ignore-docstrings = 'yes'
82+
ignore-imports = 'yes'
83+
ignore-signatures = 'yes'
84+
min-similarity-lines = 4
85+
86+
[tool.pytype]
87+
inputs = ['sigmf', 'tests']
88+
89+
[tool.black]
90+
line-length = 120
91+
92+
[tool.tox]
93+
legacy_tox_ini = '''
94+
[tox]
95+
skip_missing_interpreters = True
96+
envlist = py{37,38,39,310,311,312}
97+
98+
[testenv]
99+
usedevelop = True
100+
deps = .[test,apps]
101+
commands = coverage run
102+
'''

setup.cfg

-2
This file was deleted.

setup.py

-47
This file was deleted.

sigmf/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#
55
# SPDX-License-Identifier: LGPL-3.0-or-later
66

7-
__version__ = "1.1.5"
7+
__version__ = "1.2.0"
88

99
from .archive import SigMFArchive
1010
from .sigmffile import SigMFFile, SigMFCollection
File renamed without changes.

sigmf/apps/convert_wav.py

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright: Multiple Authors
2+
#
3+
# This file is part of SigMF. https://github.com/sigmf/sigmf-python
4+
#
5+
# SPDX-License-Identifier: LGPL-3.0-or-later
6+
7+
"""converter for wav containers"""
8+
9+
import os
10+
import tempfile
11+
import datetime
12+
import pathlib
13+
import argparse
14+
import getpass
15+
16+
from scipy.io import wavfile
17+
18+
from .. import archive
19+
from ..sigmffile import SigMFFile
20+
from ..utils import get_data_type_str
21+
22+
23+
def convert_wav(input_wav_filename, archive_filename=None, start_datetime=None, author=None):
24+
"""
25+
read a .wav and write a .sigmf archive
26+
"""
27+
samp_rate, wav_data = wavfile.read(input_wav_filename)
28+
29+
global_info = {
30+
SigMFFile.AUTHOR_KEY: getpass.getuser() if author is None else author,
31+
SigMFFile.DATATYPE_KEY: get_data_type_str(wav_data),
32+
SigMFFile.DESCRIPTION_KEY: f"Converted from {input_wav_filename}",
33+
SigMFFile.NUM_CHANNELS_KEY: 1 if len(wav_data.shape) < 2 else wav_data.shape[1],
34+
SigMFFile.RECORDER_KEY: os.path.basename(__file__),
35+
SigMFFile.SAMPLE_RATE_KEY: samp_rate,
36+
}
37+
38+
if start_datetime is None:
39+
fname = pathlib.Path(input_wav_filename)
40+
mtime = datetime.datetime.fromtimestamp(fname.stat().st_mtime)
41+
start_datetime = mtime.isoformat() + "Z"
42+
43+
capture_info = {SigMFFile.START_INDEX_KEY: 0}
44+
if start_datetime is not None:
45+
capture_info[SigMFFile.DATETIME_KEY] = start_datetime
46+
47+
tmpdir = tempfile.mkdtemp()
48+
sigmf_data_filename = input_wav_filename + archive.SIGMF_DATASET_EXT
49+
sigmf_data_path = os.path.join(tmpdir, sigmf_data_filename)
50+
wav_data.tofile(sigmf_data_path)
51+
52+
meta = SigMFFile(data_file=sigmf_data_path, global_info=global_info)
53+
meta.add_capture(0, metadata=capture_info)
54+
55+
if archive_filename is None:
56+
archive_filename = os.path.basename(input_wav_filename) + archive.SIGMF_ARCHIVE_EXT
57+
meta.tofile(archive_filename, toarchive=True)
58+
return os.path.abspath(archive_filename)
59+
60+
61+
def main():
62+
"""
63+
entry-point for sigmf_convert_wav
64+
"""
65+
parser = argparse.ArgumentParser(description="Convert .wav to .sigmf container.")
66+
parser.add_argument("input", type=str, help="Wavfile path")
67+
parser.add_argument("--author", type=str, default=None, help=f"set {SigMFFile.AUTHOR_KEY} metadata")
68+
args = parser.parse_args()
69+
70+
out_fname = convert_wav(
71+
input_wav_filename=args.input,
72+
author=args.author,
73+
)
74+
print("Wrote", out_fname)
75+
76+
77+
if __name__ == "__main__":
78+
main()

sigmf/gui.py renamed to sigmf/apps/gui.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
import logging
1111
from PySimpleGUI import *
1212

13-
from .sigmffile import SigMFFile, fromarchive, dtype_info
14-
from .archive import SIGMF_ARCHIVE_EXT
13+
from ..sigmffile import SigMFFile, fromarchive, dtype_info
14+
from ..archive import SIGMF_ARCHIVE_EXT
1515

1616
log = logging.getLogger()
1717

sigmf/sigmffile.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def __next__(self):
202202

203203
def __getitem__(self, sli):
204204
mem = self._memmap[sli] # matches behavior of numpy.ndarray.__getitem__()
205-
205+
206206
if self._return_type is None:
207207
return mem
208208

@@ -229,13 +229,15 @@ def get_num_channels(self):
229229

230230
def _is_conforming_dataset(self):
231231
"""
232-
Returns `True` if the dataset is conforming to SigMF, `False` otherwise
233-
234232
The dataset is non-conforming if the datafile contains non-sample bytes
235233
which means global trailing_bytes field is zero or not set, all captures
236234
`header_bytes` fields are zero or not set. Because we do not necessarily
237235
know the filename no means of verifying the meta/data filename roots
238236
match, but this will also check that a data file exists.
237+
238+
Returns
239+
-------
240+
`True` if the dataset is conforming to SigMF, `False` otherwise
239241
"""
240242
if self.get_global_field(self.TRAILING_BYTES_KEY, 0):
241243
return False
@@ -405,7 +407,7 @@ def get_annotations(self, index=None):
405407
annotations = self._metadata.get(self.ANNOTATION_KEY, [])
406408
if index is None:
407409
return annotations
408-
410+
409411
annotations_including_index = []
410412
for annotation in annotations:
411413
if index < annotation[self.START_INDEX_KEY]:
@@ -416,7 +418,7 @@ def get_annotations(self, index=None):
416418
if index >= annotation[self.START_INDEX_KEY] + annotation[self.LENGTH_INDEX_KEY]:
417419
# index is after annotation end -> skip
418420
continue
419-
421+
420422
annotations_including_index.append(annotation)
421423
return annotations_including_index
422424

0 commit comments

Comments
 (0)