Skip to content

Commit c06863d

Browse files
authored
Feature/overwrite behavior (#134)
* File overwrite protection w/safety-first behavior * fixes issue where tofile() has inconsistent behavior * add `overwrite` kwarg to all file write methods * New SigMFFileExistsError * add --overwrite CLI flag * increment to v1.7.0 for API change * fix tests in py3.7 * fix pyproject for most recent version of twine
1 parent 7b99746 commit c06863d

15 files changed

+304
-45
lines changed

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
name = "SigMF"
33
description = "Easily interact with Signal Metadata Format (SigMF) recordings."
44
keywords = ["gnuradio", "radio"]
5+
license = { file = "COPYING-LGPL" }
56
classifiers = [
67
"Development Status :: 5 - Production/Stable",
78
"License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
@@ -45,6 +46,7 @@ dependencies = [
4546

4647
[tool.setuptools]
4748
packages = ["sigmf"]
49+
license-files = []
4850
[tool.setuptools.dynamic]
4951
version = {attr = "sigmf.__version__"}
5052
readme = {file = ["README.md"], content-type = "text/markdown"}
@@ -100,7 +102,7 @@ profile = "black"
100102
legacy_tox_ini = '''
101103
[tox]
102104
skip_missing_interpreters = True
103-
envlist = py{37,38,39,310,311,312,313}
105+
envlist = py{37,38,39,310,311,312,313,314}
104106
105107
[testenv]
106108
usedevelop = True

sigmf/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# SPDX-License-Identifier: LGPL-3.0-or-later
66

77
# version of this python module
8-
__version__ = "1.6.2"
8+
__version__ = "1.7.0"
99
# matching version of the SigMF specification
1010
__specification__ = "1.2.6"
1111

sigmf/archive.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import tempfile
1313
from pathlib import Path
1414

15-
from .error import SigMFFileError
15+
from .error import SigMFFileError, SigMFFileExistsError
1616

1717
SIGMF_ARCHIVE_EXT = ".sigmf"
1818
SIGMF_METADATA_EXT = ".sigmf-meta"
@@ -22,11 +22,7 @@
2222

2323
class SigMFArchive:
2424
"""
25-
Archive a SigMFFile
26-
27-
A `.sigmf` file must include both valid metadata and data.
28-
If `self.data_file` is not set or the requested output file
29-
is not writable, raises `SigMFFileError`.
25+
Archive a SigMFFile into a tar file.
3026
3127
Parameters
3228
----------
@@ -35,7 +31,7 @@ class SigMFArchive:
3531
A SigMFFile object with valid metadata and data_file.
3632
3733
name : PathLike | str | bytes
38-
Path to archive file to create. If file exists, overwrite.
34+
Path to archive file to create.
3935
If `name` doesn't end in .sigmf, it will be appended.
4036
For example: if `name` == "/tmp/archive1", then the
4137
following archive will be created:
@@ -56,12 +52,21 @@ class SigMFArchive:
5652
- archive1/
5753
- archive1.sigmf-meta
5854
- archive1.sigmf-data
55+
56+
overwrite : bool, default False
57+
If False, raise exception if archive file already exists.
58+
59+
Raises
60+
------
61+
SigMFFileError
62+
If `sigmffile` has no data_file set, or if `name` is not writable.
63+
5964
"""
6065

61-
def __init__(self, sigmffile, name=None, fileobj=None):
66+
def __init__(self, sigmffile, name=None, fileobj=None, overwrite=False):
6267
is_buffer = fileobj is not None
6368
self.sigmffile = sigmffile
64-
self.path, arcname, fileobj = self._resolve(name, fileobj)
69+
self.path, arcname, fileobj = self._resolve(name, fileobj, overwrite)
6570

6671
self._ensure_data_file_set()
6772
self._validate()
@@ -106,13 +111,22 @@ def _ensure_data_file_set(self):
106111
def _validate(self):
107112
self.sigmffile.validate()
108113

109-
def _resolve(self, name, fileobj):
114+
def _resolve(self, name, fileobj, overwrite=False):
110115
"""
111116
Resolve both (name, fileobj) into (path, arcname, fileobj) given either or both.
112117
118+
Parameters
119+
----------
120+
name : PathLike | str | bytes | None
121+
Path to archive file to create.
122+
fileobj : BufferedWriter | None
123+
Open file handle object.
124+
overwrite : bool, default False
125+
If False, raise exception if archive file already exists.
126+
113127
Returns
114128
-------
115-
path : PathLike
129+
path : Path
116130
Path of the archive file.
117131
arcname : str
118132
Name of the sigmf object within the archive.
@@ -144,6 +158,10 @@ def _resolve(self, name, fileobj):
144158
raise SigMFFileError(f"Invalid extension ({path.suffix} != {SIGMF_ARCHIVE_EXT}).")
145159
arcname = path.stem
146160

161+
# check if file exists and overwrite is disabled
162+
if not overwrite and path.exists():
163+
raise SigMFFileExistsError(path, "Archive file")
164+
147165
try:
148166
fileobj = open(path, "wb")
149167
except (OSError, IOError) as exc:

sigmf/convert/__main__.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def main() -> None:
6060
exclusive_group.add_argument(
6161
"--ncd", action="store_true", help="Output .sigmf-meta only and process as a Non-Conforming Dataset (NCD)"
6262
)
63+
parser.add_argument("--overwrite", action="store_true", help="Overwrite existing output files")
6364
parser.add_argument("--version", action="version", version=f"%(prog)s v{toolversion}")
6465
args = parser.parse_args()
6566

@@ -89,11 +90,23 @@ def main() -> None:
8990

9091
if magic_bytes == b"RIFF":
9192
# WAV file
92-
_ = wav_to_sigmf(wav_path=input_path, out_path=output_path, create_archive=args.archive, create_ncd=args.ncd)
93+
_ = wav_to_sigmf(
94+
wav_path=input_path,
95+
out_path=output_path,
96+
create_archive=args.archive,
97+
create_ncd=args.ncd,
98+
overwrite=args.overwrite,
99+
)
93100

94101
elif magic_bytes == b"BLUE":
95102
# BLUE file
96-
_ = blue_to_sigmf(blue_path=input_path, out_path=output_path, create_archive=args.archive, create_ncd=args.ncd)
103+
_ = blue_to_sigmf(
104+
blue_path=input_path,
105+
out_path=output_path,
106+
create_archive=args.archive,
107+
create_ncd=args.ncd,
108+
overwrite=args.overwrite,
109+
)
97110

98111
else:
99112
raise SigMFConversionError(

sigmf/convert/blue.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -498,8 +498,9 @@ def _build_common_metadata(
498498
tuple[dict, dict]
499499
(global_info, capture_info) dictionaries.
500500
"""
501-
# helper to look up extended header values by tag
501+
502502
def get_tag(tag):
503+
"""helper to look up extended header values by tag"""
503504
for entry in h_extended:
504505
if entry["tag"] == tag:
505506
return entry["value"]
@@ -670,6 +671,7 @@ def construct_sigmf(
670671
h_extended: list,
671672
is_metadata_only: bool = False,
672673
create_archive: bool = False,
674+
overwrite: bool = False,
673675
) -> SigMFFile:
674676
"""
675677
Built & write a SigMF object from BLUE metadata.
@@ -688,6 +690,8 @@ def construct_sigmf(
688690
If True, creates a metadata-only SigMF file.
689691
create_archive : bool, optional
690692
When True, package output as SigMF archive instead of a meta/data pair.
693+
overwrite : bool, optional
694+
If False, raise exception if output files already exist.
691695
692696
Returns
693697
-------
@@ -723,12 +727,12 @@ def construct_sigmf(
723727
meta.add_capture(0, metadata=capture_info)
724728

725729
if create_archive:
726-
meta.tofile(filenames["archive_fn"], toarchive=True)
730+
meta.tofile(filenames["archive_fn"], toarchive=True, overwrite=overwrite)
727731
log.info("wrote SigMF archive to %s", filenames["archive_fn"])
728732
# metadata returned should be for this archive
729733
meta = fromfile(filenames["archive_fn"])
730734
else:
731-
meta.tofile(filenames["meta_fn"], toarchive=False)
735+
meta.tofile(filenames["meta_fn"], toarchive=False, overwrite=overwrite)
732736
log.info("wrote SigMF metadata to %s", filenames["meta_fn"])
733737

734738
log.debug("created %r", meta)
@@ -790,6 +794,7 @@ def blue_to_sigmf(
790794
out_path: Optional[str] = None,
791795
create_archive: bool = False,
792796
create_ncd: bool = False,
797+
overwrite: bool = False,
793798
) -> SigMFFile:
794799
"""
795800
Read a MIDAS Bluefile, optionally write SigMF, return associated SigMF object.
@@ -804,6 +809,8 @@ def blue_to_sigmf(
804809
When True, package output as a .sigmf archive.
805810
create_ncd : bool, optional
806811
When True, create Non-Conforming Dataset with header_bytes and trailing_bytes.
812+
overwrite : bool, optional
813+
If False, raise exception if output files already exist.
807814
808815
Returns
809816
-------
@@ -846,7 +853,7 @@ def blue_to_sigmf(
846853

847854
# write NCD metadata to specified output path if provided
848855
if out_path is not None:
849-
ncd_meta.tofile(filenames["meta_fn"])
856+
ncd_meta.tofile(filenames["meta_fn"], overwrite=overwrite)
850857
log.info("wrote SigMF non-conforming metadata to %s", filenames["meta_fn"])
851858

852859
return ncd_meta
@@ -872,6 +879,7 @@ def blue_to_sigmf(
872879
h_extended=h_extended,
873880
is_metadata_only=metadata_only,
874881
create_archive=create_archive,
882+
overwrite=overwrite,
875883
)
876884

877885
log.debug(">>>>>>>>> Fixed Header")

sigmf/convert/wav.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .. import SigMFFile
2020
from .. import __version__ as toolversion
2121
from .. import fromfile
22+
from ..error import SigMFFileExistsError
2223
from ..sigmffile import get_sigmf_filenames
2324
from ..utils import SIGMF_DATETIME_ISO8601_FMT, get_data_type_str
2425

@@ -78,6 +79,7 @@ def wav_to_sigmf(
7879
out_path: Optional[str] = None,
7980
create_archive: bool = False,
8081
create_ncd: bool = False,
82+
overwrite: bool = False,
8183
) -> SigMFFile:
8284
"""
8385
Read a wav, optionally write sigmf, return associated SigMF object.
@@ -92,6 +94,8 @@ def wav_to_sigmf(
9294
When True, package output as a .sigmf archive.
9395
create_ncd : bool, optional
9496
When True, create Non-Conforming Dataset with header_bytes and trailing_bytes.
97+
overwrite : bool, optional
98+
If False, raise exception if output files already exist.
9599
96100
Returns
97101
-------
@@ -172,7 +176,7 @@ def wav_to_sigmf(
172176
filenames = get_sigmf_filenames(out_path)
173177
output_dir = filenames["meta_fn"].parent
174178
output_dir.mkdir(parents=True, exist_ok=True)
175-
meta.tofile(filenames["meta_fn"], toarchive=False)
179+
meta.tofile(filenames["meta_fn"], toarchive=False, overwrite=overwrite)
176180
log.info("wrote SigMF non-conforming metadata to %s", filenames["meta_fn"])
177181

178182
log.debug("created %r", meta)
@@ -197,20 +201,25 @@ def wav_to_sigmf(
197201
meta = SigMFFile(data_file=data_path, global_info=global_info)
198202
meta.add_capture(0, metadata=capture_info)
199203

200-
meta.tofile(filenames["archive_fn"], toarchive=True)
204+
meta.tofile(filenames["archive_fn"], toarchive=True, overwrite=overwrite)
201205
log.info("wrote SigMF archive to %s", filenames["archive_fn"])
202206
# metadata returned should be for this archive
203207
meta = fromfile(filenames["archive_fn"])
204208
else:
205209
# write separate meta and data files
206210
data_path = filenames["data_fn"]
211+
212+
# check if data file exists when overwrite is disabled
213+
if not overwrite and data_path.exists():
214+
raise SigMFFileExistsError(data_path, "Data file")
215+
207216
wav_data.tofile(data_path)
208217
log.info("wrote SigMF dataset to %s", data_path)
209218

210219
meta = SigMFFile(data_file=data_path, global_info=global_info)
211220
meta.add_capture(0, metadata=capture_info)
212221

213-
meta.tofile(filenames["meta_fn"], toarchive=False)
222+
meta.tofile(filenames["meta_fn"], toarchive=False, overwrite=overwrite)
214223
log.info("wrote SigMF metadata to %s", filenames["meta_fn"])
215224

216225
log.debug("created %r", meta)

sigmf/error.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,14 @@ class SigMFFileError(SigMFError):
2424
"""Exceptions related to reading or writing SigMF files or archives."""
2525

2626

27+
class SigMFFileExistsError(SigMFFileError):
28+
"""Exception raised when a file already exists and overwrite is disabled."""
29+
30+
def __init__(self, file_path, file_type="File"):
31+
super().__init__(f"{file_type} {file_path} already exists. Use overwrite=True to overwrite.")
32+
self.file_path = file_path
33+
self.file_type = file_type
34+
35+
2736
class SigMFConversionError(SigMFError):
2837
"""Exceptions related to converting to SigMF format."""

0 commit comments

Comments
 (0)