Skip to content

Commit

Permalink
Drop3.6 & move all configuration into pyproject.toml (#49)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Teque5 committed Feb 14, 2024
1 parent 1386965 commit c5d194d
Show file tree
Hide file tree
Showing 14 changed files with 214 additions and 152 deletions.
9 changes: 4 additions & 5 deletions .github/workflows/main.yml
@@ -1,6 +1,6 @@
name: Python package

on:
on:
push:
pull_request:
types: [opened, synchronize]
Expand All @@ -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 }}
Expand All @@ -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
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -9,8 +9,9 @@ build/*
.eggs/*
SigMF.egg-info/*

# pytest & coverage related
# test related
.coverage
pytest.xml
coverage.xml
.tox/
htmlcov/*
27 changes: 17 additions & 10 deletions README.md
@@ -1,34 +1,41 @@
<p align="center"><img src="https://github.com/sigmf/SigMF/raw/sigmf-v1.x/logo/sigmf_logo.png" width="30%" /></p>
<p align="center"><img src="https://github.com/gnuradio/SigMF/blob/sigmf-v1.x/logo/sigmf_logo.svg" alt="Rendered SigMF Logo"/></p>

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
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
Expand Down
102 changes: 102 additions & 0 deletions 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
'''
2 changes: 0 additions & 2 deletions setup.cfg

This file was deleted.

47 changes: 0 additions & 47 deletions setup.py

This file was deleted.

2 changes: 1 addition & 1 deletion sigmf/__init__.py
Expand Up @@ -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
Expand Down
File renamed without changes.
78 changes: 78 additions & 0 deletions 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()
4 changes: 2 additions & 2 deletions sigmf/gui.py → sigmf/apps/gui.py
Expand Up @@ -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()

Expand Down
12 changes: 7 additions & 5 deletions sigmf/sigmffile.py
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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]:
Expand All @@ -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

Expand Down

0 comments on commit c5d194d

Please sign in to comment.