Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Valid subsections within a version are:

Things to be included in the next release go here.

### Added

- All functions/methods/classes now accept pathlike objects, in addition to strings ([#33](https://github.com/tektronix/tm_data_types/issues/33))

---

## v0.4.1 (2026-05-21)
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Comment thread
nfelt14 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ documentation = "https://tm-data-types.readthedocs.io"
homepage = "https://pypi.org/project/tm_data_types/"
repository = "https://github.com/tektronix/tm_data_types"


[tool.coverage.paths]
source = [
"src/tm_data_types",
Expand Down
3 changes: 2 additions & 1 deletion src/tm_data_types/files_and_formats/abstracted_file.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""The base file type which abstracts all file formats."""

from abc import ABC, abstractmethod
from pathlib import Path
from typing import Dict, Generic, Optional, TextIO, TypeVar

from bidict import bidict
Expand All @@ -21,7 +22,7 @@ class AbstractedFile(ABC, Generic[DATUM_TYPE_VAR]):

def __init__(
self,
file_path: str,
file_path: str | Path,
io_type: str,
product: Optional[InstrumentSeries] = InstrumentSeries.TEKSCOPE,
) -> None:
Expand Down
10 changes: 8 additions & 2 deletions src/tm_data_types/files_and_formats/mat/mat.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ def read_datum(self) -> WAVEFORM_TYPE:
Returns:
A waveform filled with information from the .wfm file.
"""
formatted_data = sio.loadmat(self.file_path)
file_path_str = (
self.file_path if isinstance(self.file_path, str) else self.file_path.as_posix()
)
formatted_data = sio.loadmat(file_path_str)
return self._convert_from_formatted_data(formatted_data)

# Writing
Expand All @@ -121,8 +124,11 @@ def write_datum(self, waveform: Waveform) -> None:
Args:
waveform: The waveform to pack into the .wfm file.
"""
file_path_str = (
self.file_path if isinstance(self.file_path, str) else self.file_path.as_posix()
)
formatted_data = self._convert_to_formatted_data(waveform)
sio.savemat(self.file_path, formatted_data)
sio.savemat(file_path_str, formatted_data)

################################################################################################
# Private Methods
Expand Down
42 changes: 25 additions & 17 deletions src/tm_data_types/io_factory_methods.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Factory methods to read and write to a generic file using generic data."""

import multiprocessing
import os

from pathlib import Path
from typing import List, Optional, TYPE_CHECKING

from typing_extensions import TypeVar
Expand All @@ -27,7 +27,7 @@

# pylint: disable=unused-argument
def write_file(
path: str,
file_path: str | Path,
waveform: Datum,
product: InstrumentSeries = InstrumentSeries.TEKSCOPE,
file_format: Optional[CSVFormats] = None, # noqa: ARG001
Expand All @@ -50,26 +50,29 @@ def write_file(
stored separately.

Args:
path: The path file to write to.
file_path: The path file to write to.
waveform: The waveform that is being written.
product: The product being written to.
file_format: A specialized file format we are writing as.
"""
_, path_extension = os.path.splitext(path)
file_path = Path(file_path) if isinstance(file_path, str) else file_path

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You actually don't need the if/else, just call Path(file_path)


# make sure the extension is supported
try:
file_extension = FileExtensions[path_extension.replace(".", "").upper()]
file_extension = FileExtensions[file_path.suffix.lstrip(".").upper()]
except KeyError as e:
msg = f"The {path_extension} extension cannot be written to."
msg = f"The {file_path.suffix} extension cannot be written to."
raise IOError(msg) from e

# find the format based on the waveform extension
format_class: AbstractedFile = find_class_format(file_extension, type(waveform))
# using __init__ for instantiation due to pyright confusion
format_class = format_class(path, access_type(file_extension, write=True), product)
format_class = format_class(file_path, access_type(file_extension, write=True), product)
with format_class as fd:
fd.write_datum(waveform)


def read_file(file_path: str) -> DatumAlias:
def read_file(file_path: str | Path) -> DatumAlias:
"""Read a waveform from a provided file.

Process Overview:
Expand All @@ -89,12 +92,15 @@ def read_file(file_path: str) -> DatumAlias:
Args:
file_path: The path file to read from.
"""
_, path_extension = os.path.splitext(file_path)
file_path = Path(file_path) if isinstance(file_path, str) else file_path

# ensure the extension is supported
try:
file_extension = FileExtensions[path_extension.replace(".", "").upper()]
file_extension = FileExtensions[file_path.suffix.lstrip(".").upper()]
except KeyError as e:
msg = f"The {path_extension} extension cannot be read from."
msg = f"The {file_path.suffix} extension cannot be read from."
raise IOError(msg) from e

class_formats: List[AbstractedFile] = find_class_format_list(file_extension)
for file_format in class_formats:
with file_format(file_path, access_type(file_extension, write=False)) as fd:
Expand All @@ -104,7 +110,7 @@ def read_file(file_path: str) -> DatumAlias:
raise TypeError(msg)


def read_analog_file(file_path: str) -> AnalogWaveform:
def read_analog_file(file_path: str | Path) -> AnalogWaveform:
"""Read a waveform from a provided file, type hinted as an AnalogWaveform.

Args:
Expand All @@ -113,7 +119,7 @@ def read_analog_file(file_path: str) -> AnalogWaveform:
return read_file(file_path)


def read_iq_file(file_path: str) -> IQWaveform:
def read_iq_file(file_path: str | Path) -> IQWaveform:
"""Read a waveform from a provided file, type hinted as an IQWaveform.

Args:
Expand All @@ -131,7 +137,7 @@ def _write_files(
"""Write a list of waveforms to a list of provided files.

Args:
file_paths: The path file to write to.
file_paths: The list of path files to write to.
datums: The datum that is being written.
product: The product being written to.
file_format: A specialized file format we are writing as.
Expand All @@ -141,7 +147,7 @@ def _write_files(


def write_files_in_parallel(
file_paths: List[str],
file_paths: List[str] | List[Path],
datums: List[Datum],
force_process_count: int = 4,
product: InstrumentSeries = InstrumentSeries.TEKSCOPE,
Expand All @@ -160,7 +166,7 @@ def write_files_in_parallel(
This method is particularly useful for saving multiple waveform files efficiently.

Args:
file_paths: The path file to write to.
file_paths: The list of path files to write to.
datums: The datum that is being written.
force_process_count: The number of processes that will be created.
product: The product being written to.
Expand Down Expand Up @@ -207,7 +213,9 @@ def _read_files(file_paths: str, file_queue: multiprocessing.Queue) -> None:
file_queue.put((file_path, read_file(file_path)))


def read_files_in_parallel(file_paths: List[str], force_process_count: int = 4) -> List[Datum]:
def read_files_in_parallel(
file_paths: List[str] | List[Path], force_process_count: int = 4
) -> List[Datum]:
"""Read a list of files in parallel.

This method allows for the parallel reading of multiple waveform files.
Expand Down
5 changes: 3 additions & 2 deletions tests/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from collections.abc import Callable
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import List, Optional, Tuple

import matplotlib.pyplot as plt
Expand Down Expand Up @@ -35,7 +36,7 @@ class Performance:
reads_per_second: NDArray


def write_files_serial(file_paths: List[str], datums: List[AnalogWaveform]) -> None:
def write_files_serial(file_paths: List[str] | List[Path], datums: List[AnalogWaveform]) -> None:
"""Write to the provided file paths using the waveforms serially.

Args:
Expand All @@ -46,7 +47,7 @@ def write_files_serial(file_paths: List[str], datums: List[AnalogWaveform]) -> N
write_file(file_path, datum)


def read_files_serial(file_paths: List[str]) -> None:
def read_files_serial(file_paths: List[str] | List[Path]) -> None:
"""Read the provided file paths serially.

Args:
Expand Down
63 changes: 58 additions & 5 deletions tests/test_tm_data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def test_parallel(tmp_path: Path) -> None:
)
waveform_info[waveform_path.as_posix()] = waveform

# test file paths as strings
write_files_in_parallel(list(waveform_info.keys()), list(waveform_info.values()))

if read_info := read_files_in_parallel(list(waveform_info.keys())):
Expand All @@ -95,6 +96,19 @@ def test_parallel(tmp_path: Path) -> None:
msg = "No Files written/read."
raise IOError(msg)

# test file paths as Path objects
path_keys = [Path(key) for key in waveform_info]
write_files_in_parallel(list(path_keys), list(waveform_info.values()))

if read_info := read_files_in_parallel(list(path_keys)):
for file_path, waveform in read_info:
assert np.array_equal(
waveform.y_axis_values, waveform_info[file_path.as_posix()].y_axis_values
)
else:
msg = "No Files written/read."
raise IOError(msg)
Comment thread
nfelt14 marked this conversation as resolved.


@pytest.mark.parametrize(
("waveform_type", "waveform_meta_info"),
Expand All @@ -116,11 +130,20 @@ def test_wfm(

waveform.y_axis_values = values
waveform.y_axis_spacing = 1 / type_max(np.dtype(np.int16))
write_file(waveform_path.as_posix(), waveform)

# test write_file with string
write_file(waveform_path.as_posix(), waveform)
with open(waveform_path, "rb+") as wfm:
raw_test_data = wfm.read()
with open(golden_path, "rb+") as wfm:
golden_format_data = wfm.read()

assert raw_test_data == golden_format_data

# test write_file with Path object
write_file(waveform_path, waveform)
with open(waveform_path, "rb+") as wfm:
raw_test_data = wfm.read()
with open(golden_path, "rb+") as wfm:
golden_format_data = wfm.read()

Expand All @@ -129,9 +152,9 @@ def test_wfm(

def read_write_read(
vertical_data: str,
waveform_path: str,
data_path: str,
temp_path: str,
waveform_path: str | Path,
data_path: str | Path,
temp_path: str | Path,
) -> None:
"""Read a file, then write the waveform from the file, then read it again.

Expand Down Expand Up @@ -170,7 +193,16 @@ def read_write_read(
atol=0.0005,
),
)
if waveform_path.rsplit(".", maxsplit=1)[-1] == temp_path.rsplit(".", maxsplit=1)[-1]:

# Extract extensions
ext1 = (
waveform_path.rsplit(".", maxsplit=1)[-1]
if isinstance(waveform_path, str)
else waveform_path.suffix
)
ext2 = temp_path.rsplit(".", maxsplit=1)[-1] if isinstance(temp_path, str) else temp_path.suffix

if ext1 == ext2:
assert np.array_equal(getattr(re_read_waveform, vertical_data), read_wfm_values)
else:
re_read_values = getattr(re_read_waveform, vertical_data)
Expand Down Expand Up @@ -207,6 +239,15 @@ def test_analog(tmp_path: Path) -> None:
temporary_path.as_posix(),
)

# verify that Path objects are accepted in addition to strings
Comment thread
nfelt14 marked this conversation as resolved.
path_waveform = tmp_path / f"test_analog_pathlib.{extensions[0]}"
read_write_read(
"y_axis_values",
Path(waveform_path + extensions[0]), # only need to test one file to see if it works
data_path,
path_waveform,
)


def test_iq(tmp_path: Path) -> None:
"""Test to see if IQ waveforms will return the same data when saved and loaded."""
Expand All @@ -232,6 +273,12 @@ def test_iq(tmp_path: Path) -> None:
temporary_path.as_posix(),
)

# verify that Path objects are accepted in addition to strings
path_waveform = tmp_path / f"test_iq_pathlib.{extensions[0]}"
read_write_read(
"interleaved_iq_axis_values", Path(waveform_path + extensions[0]), data_path, path_waveform
)


def test_digital(tmp_path: Path) -> None:
"""Test to see if digital waveforms will return the same data when saved and loaded."""
Expand All @@ -257,6 +304,12 @@ def test_digital(tmp_path: Path) -> None:
temporary_path.as_posix(),
)

# verify that Path objects are accepted in addition to strings
path_waveform = tmp_path / f"test_digital_pathlib.{extensions[0]}"
read_write_read(
"y_axis_byte_values", Path(waveform_path + extensions[0]), data_path, path_waveform
)


def test_data() -> None: # pylint: disable=too-many-locals
"""Test if normalized data is correctly represented, and that data types can be converted."""
Expand Down