From 2dc504fad8d013ccb8751417f17c8094f7534f6f Mon Sep 17 00:00:00 2001 From: Dani Bodor Date: Mon, 11 Mar 2024 12:59:17 +0100 Subject: [PATCH 1/9] remove obsolete helper module --- eitprocessing/helper.py | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 eitprocessing/helper.py diff --git a/eitprocessing/helper.py b/eitprocessing/helper.py deleted file mode 100644 index 889df6eb1..000000000 --- a/eitprocessing/helper.py +++ /dev/null @@ -1,2 +0,0 @@ -class NotConsecutive(Exception): - """Raised when trying to concatenate non-consecutive datasets.""" From 134fafb62777564666c6e1ea9c0a76fc0054a3a8 Mon Sep 17 00:00:00 2001 From: Dani Bodor Date: Mon, 11 Mar 2024 13:10:38 +0100 Subject: [PATCH 2/9] add type hints in reader.py --- eitprocessing/binreader/reader.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/eitprocessing/binreader/reader.py b/eitprocessing/binreader/reader.py index 16d122d07..c747263a9 100644 --- a/eitprocessing/binreader/reader.py +++ b/eitprocessing/binreader/reader.py @@ -34,12 +34,12 @@ def read_array( data = self._read_full_type_code(full_type_code) return np.array(data, dtype=cast) - def read_string(self, length=1): + def read_string(self, length: int = 1) -> str: full_type_code = f"{length}s" data = self._read_full_type_code(full_type_code) return data[0].decode().rstrip() - def _read_full_type_code(self, full_type_code) -> tuple[Any, ...]: + def _read_full_type_code(self, full_type_code: str) -> tuple[Any, ...]: if self.endian: if self.endian not in ["little", "big"]: msg = f"Endian type '{self.endian}' not recognized. Allowed values are 'little' and 'big'." @@ -58,19 +58,19 @@ def float32(self) -> float: def float64(self) -> float: return self.read_single(type_code="d", cast=float) - def npfloat32(self, length=1) -> NDArray[np.float32]: + def npfloat32(self, length: int = 1) -> NDArray[np.float32]: return self.read_array(type_code="f", cast=np.float32, length=length) - def npfloat64(self, length=1) -> NDArray[np.float64]: + def npfloat64(self, length: int = 1) -> NDArray[np.float64]: return self.read_array(type_code="d", cast=np.float64, length=length) def int32(self) -> int: return self.read_single(type_code="i", cast=int) - def npint32(self, length=1) -> NDArray[np.int32]: + def npint32(self, length: int = 1) -> NDArray[np.int32]: return self.read_array(type_code="i", cast=np.int32, length=length) - def string(self, length=1) -> str: + def string(self, length: int = 1) -> str: return self.read_string(length=length) def uint8(self) -> int: From 897b70b7052ec511652e99625cdc09d9540c8eeb Mon Sep 17 00:00:00 2001 From: Dani Bodor Date: Mon, 11 Mar 2024 13:17:18 +0100 Subject: [PATCH 3/9] ignore linting in unfinished modules --- eitprocessing/plotting/animate.py | 3 +++ eitprocessing/plotting/plot.py | 3 +++ tests/mixins/test_eq.py | 2 ++ tests/mixins/test_slicing.py | 2 ++ tests/test_butterworth_filter.py | 2 ++ 5 files changed, 12 insertions(+) diff --git a/eitprocessing/plotting/animate.py b/eitprocessing/plotting/animate.py index 47f64a6f6..a6019e0c1 100644 --- a/eitprocessing/plotting/animate.py +++ b/eitprocessing/plotting/animate.py @@ -3,6 +3,9 @@ from eitprocessing.eit_data.eit_data_variant import EITDataVariant +# TODO: remove line below to activate linting +# ruff: noqa + def animate_EITDataVariant( eit_data_variant: EITDataVariant, diff --git a/eitprocessing/plotting/plot.py b/eitprocessing/plotting/plot.py index ea4803bd1..33f0fcb22 100644 --- a/eitprocessing/plotting/plot.py +++ b/eitprocessing/plotting/plot.py @@ -1,5 +1,8 @@ from matplotlib import pyplot as plt +# TODO: remove line below to activate linting +# ruff: noqa + def plot_waveforms(self, waveforms=None) -> None: if waveforms is None: diff --git a/tests/mixins/test_eq.py b/tests/mixins/test_eq.py index 66a37ee4f..8555ecc65 100644 --- a/tests/mixins/test_eq.py +++ b/tests/mixins/test_eq.py @@ -1,4 +1,6 @@ # OLD FILE. TESTS NOT YET FUNCTIONAL +# TODO: remove line below to activate linting +# ruff: noqa import bisect import os diff --git a/tests/mixins/test_slicing.py b/tests/mixins/test_slicing.py index 8592dde01..086e5e237 100644 --- a/tests/mixins/test_slicing.py +++ b/tests/mixins/test_slicing.py @@ -4,6 +4,8 @@ import pytest from eitprocessing.eit_data.draeger import DraegerEITData +# TODO: remove line below to activate linting +# ruff: noqa environment = os.environ.get( "EIT_PROCESSING_TEST_DATA", diff --git a/tests/test_butterworth_filter.py b/tests/test_butterworth_filter.py index 9ddf4b402..e625b33d6 100644 --- a/tests/test_butterworth_filter.py +++ b/tests/test_butterworth_filter.py @@ -13,6 +13,8 @@ ) SpecifiedFilter: TypeAlias = type[LowPassFilter | HighPassFilter | BandPassFilter | BandStopFilter] +# TODO: remove line below to activate linting +# ruff: noqa @pytest.fixture() From a62ad9e20a3ccc58690c3f8786df5a31b5e9de5b Mon Sep 17 00:00:00 2001 From: Dani Bodor Date: Mon, 11 Mar 2024 13:17:25 +0100 Subject: [PATCH 4/9] add missing init --- eitprocessing/plotting/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 eitprocessing/plotting/__init__.py diff --git a/eitprocessing/plotting/__init__.py b/eitprocessing/plotting/__init__.py new file mode 100644 index 000000000..e69de29bb From 6beba5891476b59c6c4b862c14fa5cffdcec8059 Mon Sep 17 00:00:00 2001 From: Dani Bodor Date: Mon, 11 Mar 2024 13:27:19 +0100 Subject: [PATCH 5/9] add missing docstrings --- eitprocessing/eit_data/event.py | 2 ++ eitprocessing/eit_data/phases.py | 8 +++++--- eitprocessing/mixins/slicing.py | 2 +- eitprocessing/sparse_data/__init__.py | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/eitprocessing/eit_data/event.py b/eitprocessing/eit_data/event.py index b5a2a0f26..11c9d4dbf 100644 --- a/eitprocessing/eit_data/event.py +++ b/eitprocessing/eit_data/event.py @@ -3,6 +3,8 @@ @dataclass class Event: + """Single time point event registered during an EIT measurement.""" + index: int time: float marker: int diff --git a/eitprocessing/eit_data/phases.py b/eitprocessing/eit_data/phases.py index e13389c04..c30bf69c0 100644 --- a/eitprocessing/eit_data/phases.py +++ b/eitprocessing/eit_data/phases.py @@ -3,20 +3,22 @@ @dataclass class PhaseIndicator: + """Parent class for phase indications.""" + index: int time: float @dataclass class MinValue(PhaseIndicator): - pass + """Automatically registered local minimum of an EIT measurement.""" @dataclass class MaxValue(PhaseIndicator): - pass + """Automatically registered local maximum of an EIT measurement.""" @dataclass class QRSMark(PhaseIndicator): - pass + """Automatically registered QRS mark an EIT measurement from a Timpel device.""" diff --git a/eitprocessing/mixins/slicing.py b/eitprocessing/mixins/slicing.py index c44bcb6e3..e781d3bb6 100644 --- a/eitprocessing/mixins/slicing.py +++ b/eitprocessing/mixins/slicing.py @@ -84,7 +84,7 @@ class SelectByTime(SelectByIndex): time: NDArray - def select_by_time( + def select_by_time( # noqa: D417 self, start_time: float | None = None, end_time: float | None = None, diff --git a/eitprocessing/sparse_data/__init__.py b/eitprocessing/sparse_data/__init__.py index 473e5c4d6..701b44238 100644 --- a/eitprocessing/sparse_data/__init__.py +++ b/eitprocessing/sparse_data/__init__.py @@ -1,2 +1,2 @@ class SparseData: - ... + """SparseData.""" From 71a8602d6513d64a9ca6e9984be4e13cecb6b2c8 Mon Sep 17 00:00:00 2001 From: Dani Bodor Date: Mon, 11 Mar 2024 13:27:55 +0100 Subject: [PATCH 6/9] ignore missing docstrings in reader --- eitprocessing/binreader/reader.py | 63 ++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/eitprocessing/binreader/reader.py b/eitprocessing/binreader/reader.py index c747263a9..e65a96b2b 100644 --- a/eitprocessing/binreader/reader.py +++ b/eitprocessing/binreader/reader.py @@ -1,7 +1,7 @@ import io import struct from dataclasses import dataclass -from typing import Any, TypeVar +from typing import Any, Literal, TypeVar import numpy as np from numpy.typing import NDArray @@ -12,14 +12,45 @@ @dataclass class Reader: + """Helper class for reading binary files from disk. + + Args: + file_handle: a buffered reader handle, e.g. the result of the `open()` function. + endian: the endianness of the binary data. Either 'little' or 'big', or None. + """ + file_handle: io.BufferedReader - endian: str | None = None + endian: Literal["little", "big"] | None = None def read_single(self, type_code: str, cast: type[T]) -> T: + """Read and return a single unit of the given type code. + + The type of data to be read should be provided as a single typ code. See + https://docs.python.org/3.10/library/struct.html#byte-order-size-and-alignment for a list of available type + codes. + + A unit returns a single value, and can be one or more bytes of data. E.g. requesting a signed 32-bit integer + ('q') will result in reading 8 bytes of data. + + `cast` should be a type, e.g. `int` or `float` used to cast the value to the proper type. + + Args: + type_code: singular type code. + cast: the associated type. + """ data = self._read_full_type_code(type_code) return cast(data[0]) def read_list(self, type_code: str, cast: type[T], length: int) -> list[T]: + """Read multiple values of the same type and return as list. + + See `read_single()`. + + Args: + type_code: singular type code. + cast: the associated type. + length: number of values to be read. + """ full_type_code = f"{length}{type_code}" data = self._read_full_type_code(full_type_code) return [cast(d) for d in data] @@ -30,16 +61,31 @@ def read_array( cast: type[N], length: int, ) -> NDArray[N]: + """Read multiple values of the same type and return as NumPy array. + + See `read_list()`. + """ full_type_code = f"{length}{type_code}" data = self._read_full_type_code(full_type_code) return np.array(data, dtype=cast) def read_string(self, length: int = 1) -> str: + """Read and return a string with a given length. + + Reads `length` characters of type code 's' and returns as a string. When length is not provided, a single + character is returned. + + Args: + length: number of characters. + """ full_type_code = f"{length}s" data = self._read_full_type_code(full_type_code) return data[0].decode().rstrip() + string = read_string + def _read_full_type_code(self, full_type_code: str) -> tuple[Any, ...]: + """Read the data associated with the type code.""" if self.endian: if self.endian not in ["little", "big"]: msg = f"Endian type '{self.endian}' not recognized. Allowed values are 'little' and 'big'." @@ -53,34 +99,41 @@ def _read_full_type_code(self, full_type_code: str) -> tuple[Any, ...]: return struct.unpack(full_type_code, packed_data) def float32(self) -> float: + """Read and return a single signed 32-bit floating point value.""" return self.read_single(type_code="f", cast=float) def float64(self) -> float: + """Read and return a single signed 64-bit floating point value.""" return self.read_single(type_code="d", cast=float) def npfloat32(self, length: int = 1) -> NDArray[np.float32]: + """Read and return an array of signed 32-bit floating point values.""" return self.read_array(type_code="f", cast=np.float32, length=length) def npfloat64(self, length: int = 1) -> NDArray[np.float64]: + """Read and return an array of signed 64-bit floating point values.""" return self.read_array(type_code="d", cast=np.float64, length=length) def int32(self) -> int: + """Read and return a single signed 32-bit integer value.""" return self.read_single(type_code="i", cast=int) def npint32(self, length: int = 1) -> NDArray[np.int32]: + """Read and return an array of signed 32-bit integer values.""" return self.read_array(type_code="i", cast=np.int32, length=length) - def string(self, length: int = 1) -> str: - return self.read_string(length=length) - def uint8(self) -> int: + """Read and return a single unsigned 8-bit integer value.""" return self.read_single(type_code="B", cast=int) def uint16(self) -> int: + """Read and return a single unsigned 16-bit integer value.""" return self.read_single(type_code="H", cast=int) def uint32(self) -> int: + """Read and return a single unsigned 32-bit integer value.""" return self.read_single(type_code="I", cast=int) def uint64(self) -> int: + """Read and return a single unsigned 64-bit integer value.""" return self.read_single(type_code="Q", cast=int) From d2bfff99fcccfea8285620e9305670641cbb213c Mon Sep 17 00:00:00 2001 From: Dani Bodor Date: Mon, 11 Mar 2024 13:31:07 +0100 Subject: [PATCH 7/9] fix formatting snag --- eitprocessing/eit_data/__init__.py | 3 ++- eitprocessing/eit_data/draeger.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/eitprocessing/eit_data/__init__.py b/eitprocessing/eit_data/__init__.py index 577e0191f..8707b448a 100644 --- a/eitprocessing/eit_data/__init__.py +++ b/eitprocessing/eit_data/__init__.py @@ -151,7 +151,8 @@ def _from_path( first_frame: int | None = None, max_frames: int | None = None, return_non_eit_data: bool = False, - ) -> DataCollection | tuple[DataCollection, DataCollection, DataCollection]: ... + ) -> DataCollection | tuple[DataCollection, DataCollection, DataCollection]: + ... @staticmethod def _ensure_path_list(path: str | Path | list[str | Path]) -> list[Path]: diff --git a/eitprocessing/eit_data/draeger.py b/eitprocessing/eit_data/draeger.py index 3a4a57b4a..38d9f895d 100644 --- a/eitprocessing/eit_data/draeger.py +++ b/eitprocessing/eit_data/draeger.py @@ -180,7 +180,7 @@ def _read_frame( # noqa: PLR0913 event_text = reader.string(length=30) timing_error = reader.int32() - frame_medibus_data = reader.npfloat32(length=52) # noqa; + frame_medibus_data = reader.npfloat32(length=52) if index < 0: # do not keep any loaded data, just return the event marker From 9ffb0eda43737e3a05dd4748b4f1691528fc678e Mon Sep 17 00:00:00 2001 From: Dani Bodor Date: Mon, 11 Mar 2024 13:36:23 +0100 Subject: [PATCH 8/9] ci: update ruff settings --- pyproject.toml | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c44b6fd82..2f89a995b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "scipy >= 1.10.1", "tqdm >= 4.65.0", "strenum >= 0.4.10", - "ruff >= 0.1.13", + "ruff >= 0.3", ] [project.optional-dependencies] @@ -94,6 +94,8 @@ extras = dev [tool.ruff] line-length = 120 + +[tool.ruff.lint] select = ["ALL"] ignore = [ # Unwanted (potentially) @@ -131,20 +133,9 @@ ignore = [ "D413", ] -# Allow autofix for all enabled rules. +# Set autofixing. fixable = ["ALL"] unfixable = ["F401"] # unused imports (should not disappear while editing) - -[tool.ruff.lint.per-file-ignores] -"*.ipynb" = ["ERA001"] # Commented out code -"tests/*" = [ - "S101", # Use of `assert` detected - "ANN201", # Missing return type - "D103", # Missing function docstring -] -"docs/*" = ["ALL"] - -[tool.ruff.lint] extend-safe-fixes = [ "D415", # First line should end with a period, question mark, or exclamation point "D300", # Use triple double quotes `"""` @@ -156,11 +147,17 @@ extend-safe-fixes = [ "B006", # Mutable default argument "TID252", # Absolute imports over relative imports ] -pylint.max-args = 7 -[tool.ruff.isort] -known-first-party = ["eitprocessing"] +# Override default settings for specific rules +pylint.max-args = 7 +flake8-tidy-imports.ban-relative-imports = "all" # Disallow all relative imports. +isort.known-first-party = ["eitprocessing"] -[tool.ruff.lint.flake8-tidy-imports] -# Disallow all relative imports. -ban-relative-imports = "all" +[tool.ruff.lint.per-file-ignores] +"*.ipynb" = ["ERA001"] # Commented out code +"tests/*" = [ + "S101", # Use of `assert` detected + "ANN201", # Missing return type + "D103", # Missing function docstring +] +"docs/*" = ["ALL"] From 72a9856eff8ed186f4d341bcd52fe6521282f2ef Mon Sep 17 00:00:00 2001 From: Dani Bodor Date: Mon, 11 Mar 2024 18:39:47 +0100 Subject: [PATCH 9/9] linting fixes in filter subpackage --- eitprocessing/filters/__init__.py | 14 ++++++++++---- eitprocessing/filters/butterworth_filters.py | 19 ++++++++----------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/eitprocessing/filters/__init__.py b/eitprocessing/filters/__init__.py index ba04c123a..d1c3a3182 100644 --- a/eitprocessing/filters/__init__.py +++ b/eitprocessing/filters/__init__.py @@ -1,9 +1,15 @@ +from abc import ABC, abstractmethod from typing import NoReturn +import numpy.typing as npt + + +class TimeDomainFilter(ABC): + """Parent class for time domain filters.""" -class TimeDomainFilter: available_in_gui = True - def apply_filter(self, input_data) -> NoReturn: - msg = "Implement in subclass" - raise NotImplementedError(msg) + @abstractmethod + def apply_filter(self, input_data: npt.ArrayLike) -> NoReturn: + """Apply the filter to the input data.""" + ... diff --git a/eitprocessing/filters/butterworth_filters.py b/eitprocessing/filters/butterworth_filters.py index de101f8ed..a360a95d9 100644 --- a/eitprocessing/filters/butterworth_filters.py +++ b/eitprocessing/filters/butterworth_filters.py @@ -55,7 +55,7 @@ class ButterworthFilter(TimeDomainFilter): sample_frequency: float ignore_max_order: InitVar[bool] = False - def __post_init__(self, ignore_max_order): + def __post_init__(self, ignore_max_order: bool): self._check_init(ignore_max_order) self._set_filter_type_class() @@ -73,9 +73,8 @@ def _set_filter_type_class(self) -> None: cls = FILTER_TYPES[self.filter_type] self.__class__ = cls - def _check_init(self, ignore_max_order) -> None: - """ - Check the arguments of __init__ and raise exceptions when they don't meet requirements. + def _check_init(self, ignore_max_order: bool) -> None: # noqa:C901 + """Check the arguments of __init__ and raise exceptions when they don't meet requirements. Raises: ValueError: if the `filter_type` is unknown. @@ -92,24 +91,22 @@ def _check_init(self, ignore_max_order) -> None: """ if self.filter_type in ("lowpass", "highpass"): if not isinstance(self.cutoff_frequency, int | float): - msg = "Invalid `cutoff_frequency`. " f"Must be an integer or float, not {type(self.cutoff_frequency)}." + msg = f"Invalid `cutoff_frequency`. Must be an integer or float, not {type(self.cutoff_frequency)}." raise TypeError(msg) elif self.filter_type in ("bandpass", "bandstop"): if isinstance(self.cutoff_frequency, list): self.cutoff_frequency = tuple(self.cutoff_frequency) elif not isinstance(self.cutoff_frequency, tuple): - msg = "Invalid `cutoff_frequency`. " f"Must be a tuple, not {type(self.cutoff_frequency)}." + msg = f"Invalid `cutoff_frequency`. Must be a tuple, not {type(self.cutoff_frequency)}." raise TypeError(msg) - if len(self.cutoff_frequency) != 2: - msg = f"Invalid `cutoff_frequency` ({self.cutoff_frequency}). " "Must have length 2." + if len(self.cutoff_frequency) != 2: # noqa: PLR2004 + msg = f"Invalid `cutoff_frequency` ({self.cutoff_frequency}). Must have length 2." raise ValueError(msg) if not all(isinstance(value, int | float) for value in self.cutoff_frequency): - msg = ( - f"Invalid `cutoff_frequency` ({self.cutoff_frequency}). " "Must be a tuple containing two numbers." - ) + msg = f"Invalid `cutoff_frequency` ({self.cutoff_frequency}). Must be a tuple containing two numbers." raise TypeError(msg) else: msg = f"Invalid `filter_type` ({self.filter_type}). Must be one of {', '.join(FILTER_TYPES.keys())}."