From 160db25396f10030a4aaf51d7ea194d5ac8474a5 Mon Sep 17 00:00:00 2001 From: anilbey Date: Tue, 16 Jan 2024 13:34:16 +0100 Subject: [PATCH] Add type stub for cppcore (#358) * create tox lint environment with mypy * Fix: type error in units module * Fix: remove unused convert.py in tests * add stub for cppcore * add efel/*.pyi to MANIFEST.in * merge get_cpp_feature and _get_cpp_feature * type annotate _get_cpp_data * update CHANGELOG.md --- .github/workflows/test.yml | 15 +++++++ CHANGELOG.md | 4 ++ MANIFEST.in | 1 + efel/api.py | 18 +------- efel/cppcore.pyi | 12 +++++ efel/pyfeatures/pyfeatures.py | 63 +++++++++++++++------------ efel/units/__init__.py | 4 ++ tests/test_units.py | 16 ++++++- tests/testdata/allfeatures/convert.py | 19 -------- tox.ini | 14 ++++-- 10 files changed, 97 insertions(+), 69 deletions(-) create mode 100644 efel/cppcore.pyi delete mode 100644 tests/testdata/allfeatures/convert.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fdad0bc4..5f058375 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -86,3 +86,18 @@ jobs: pip install tox tox-gh-actions - name: Run tox run: tox -e docs + + lint: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools + pip install tox tox-gh-actions + - name: Run tox + run: tox -e lint diff --git a/CHANGELOG.md b/CHANGELOG.md index 1df8d572..42caa5f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [5.5.3] - 2024-01 + +- Add type stub for cppcore module to make Python recognise the C++ functions' arguments and return values. + ## [5.5.0] - 2024-01 ### C++ changes diff --git a/MANIFEST.in b/MANIFEST.in index 59efb407..bbb7ca72 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,3 +4,4 @@ include LICENSE.txt include COPYING include COPYING.lesser include AUTHORS.txt +include efel/*.pyi diff --git a/efel/api.py b/efel/api.py index f3d33415..9f070964 100644 --- a/efel/api.py +++ b/efel/api.py @@ -33,6 +33,7 @@ import efel.cppcore as cppcore import efel.pyfeatures as pyfeatures +from efel.pyfeatures.pyfeatures import get_cpp_feature """ Disabling cppcore importerror override, it confuses users in case the error @@ -434,23 +435,6 @@ def _get_feature_values_serial(trace_featurenames): return featureDict -def get_cpp_feature(featureName, raise_warnings=None): - """Return value of feature implemented in cpp""" - cppcoreFeatureValues = list() - exitCode = cppcore.getFeature(featureName, cppcoreFeatureValues) - - if exitCode < 0: - if raise_warnings: - import warnings - warnings.warn( - "Error while calculating feature %s: %s" % - (featureName, cppcore.getgError()), - RuntimeWarning) - return None - else: - return numpy.array(cppcoreFeatureValues) - - def getMeanFeatureValues(traces, featureNames, raise_warnings=True): """Convenience function that returns mean values from getFeatureValues() diff --git a/efel/cppcore.pyi b/efel/cppcore.pyi new file mode 100644 index 00000000..f64c8c4b --- /dev/null +++ b/efel/cppcore.pyi @@ -0,0 +1,12 @@ +def Initialize(depfilename: str, outfilename: str) -> int: ... +def getFeature(feature_name: str, values: list) -> int: ... +def getFeatureInt(feature_name: str, values: list[int]) -> int: ... +def getFeatureDouble(feature_name: str, values: list[float]) -> int: ... +def getMapIntData(data_name: str) -> list[int]: ... +def getMapDoubleData(data_name: str) -> list[float]: ... +def setFeatureInt(feature_name: str, values: list[int]) -> int: ... +def setFeatureDouble(feature_name: str, values: list[float]) -> float: ... +def setFeatureString(feature_name: str, value: str) -> int: ... +def featuretype(feature_name: str) -> str: ... +def getgError() -> str: ... +def getFeatureNames(feature_names: list[str]) -> None: ... diff --git a/efel/pyfeatures/pyfeatures.py b/efel/pyfeatures/pyfeatures.py index 9f06c916..6d7105c0 100644 --- a/efel/pyfeatures/pyfeatures.py +++ b/efel/pyfeatures/pyfeatures.py @@ -29,7 +29,7 @@ from typing_extensions import deprecated import numpy -import efel.cppcore +from efel import cppcore from numpy.fft import * @@ -60,12 +60,12 @@ def voltage(): """Get voltage trace""" - return _get_cpp_feature("voltage") + return get_cpp_feature("voltage") def time(): """Get time trace""" - return _get_cpp_feature("time") + return get_cpp_feature("time") @deprecated("Use spike_count instead.") @@ -75,7 +75,7 @@ def Spikecount() -> numpy.ndarray: def spike_count() -> numpy.ndarray: """Get spike count.""" - peak_indices = _get_cpp_feature("peak_indices") + peak_indices = get_cpp_feature("peak_indices") if peak_indices is None: return numpy.array([0]) return numpy.array([peak_indices.size]) @@ -90,7 +90,7 @@ def spike_count_stimint() -> numpy.ndarray: """Get spike count within stimulus interval.""" stim_start = _get_cpp_data("stim_start") stim_end = _get_cpp_data("stim_end") - peak_times = _get_cpp_feature("peak_time") + peak_times = get_cpp_feature("peak_time") if peak_times is None: return numpy.array([0]) @@ -105,7 +105,7 @@ def trace_check() -> numpy.ndarray | None: """ stim_start = _get_cpp_data("stim_start") stim_end = _get_cpp_data("stim_end") - peak_times = _get_cpp_feature("peak_time") + peak_times = get_cpp_feature("peak_time") if peak_times is None: # If no spikes, then no problem return numpy.array([0]) # Check if there are no spikes or if all spikes are within the stimulus interval @@ -117,7 +117,7 @@ def trace_check() -> numpy.ndarray | None: def burst_number() -> numpy.ndarray: """The number of bursts.""" - burst_mean_freq = _get_cpp_feature("burst_mean_freq") + burst_mean_freq = get_cpp_feature("burst_mean_freq") if burst_mean_freq is None: return numpy.array([0]) return numpy.array([burst_mean_freq.size]) @@ -132,7 +132,7 @@ def strict_burst_number() -> numpy.ndarray: The burst detection can be fine-tuned by changing the setting strict_burst_factor. Default value is 2.0.""" - burst_mean_freq = _get_cpp_feature("strict_burst_mean_freq") + burst_mean_freq = get_cpp_feature("strict_burst_mean_freq") if burst_mean_freq is None: return numpy.array([0]) return numpy.array([burst_mean_freq.size]) @@ -144,11 +144,11 @@ def impedance(): dt = _get_cpp_data("interp_step") Z_max_freq = _get_cpp_data("impedance_max_freq") voltage_trace = voltage() - holding_voltage = _get_cpp_feature("voltage_base") + holding_voltage = get_cpp_feature("voltage_base") normalized_voltage = voltage_trace - holding_voltage current_trace = current() if current_trace is not None: - holding_current = _get_cpp_feature("current_base") + holding_current = get_cpp_feature("current_base") normalized_current = current_trace - holding_current n_spikes = spike_count() if n_spikes < 1: # if there is no spikes in ZAP @@ -174,12 +174,12 @@ def impedance(): def current(): """Get current trace""" - return _get_cpp_feature("current") + return get_cpp_feature("current") def ISIs(): """Get all ISIs.""" - peak_times = _get_cpp_feature("peak_time") + peak_times = get_cpp_feature("peak_time") if peak_times is None: return None else: @@ -191,7 +191,7 @@ def initburst_sahp_vb(): # Required cpp features initburst_sahp_value = initburst_sahp() - voltage_base = _get_cpp_feature("voltage_base") + voltage_base = get_cpp_feature("voltage_base") if initburst_sahp_value is None or voltage_base is None or \ len(initburst_sahp_value) != 1 or len(voltage_base) != 1: @@ -205,7 +205,7 @@ def initburst_sahp_ssse(): # Required cpp features initburst_sahp_value = initburst_sahp() - ssse = _get_cpp_feature("steady_state_voltage_stimend") + ssse = get_cpp_feature("steady_state_voltage_stimend") if initburst_sahp_value is None or ssse is None or \ len(initburst_sahp_value) != 1 or len(ssse) != 1: @@ -218,10 +218,10 @@ def initburst_sahp(): """SlowAHP voltage after initial burst""" # Required cpp features - voltage = _get_cpp_feature("voltage") - time = _get_cpp_feature("time") + voltage = get_cpp_feature("voltage") + time = get_cpp_feature("time") time = time[:len(voltage)] - peak_times = _get_cpp_feature("peak_time") + peak_times = get_cpp_feature("peak_time") # Required python features all_isis = ISIs() @@ -310,9 +310,9 @@ def depol_block(): stim_end = _get_cpp_data("stim_end") # Required cpp features - voltage = _get_cpp_feature("voltage") - time = _get_cpp_feature("time") - AP_begin_voltage = _get_cpp_feature("AP_begin_voltage") + voltage = get_cpp_feature("voltage") + time = get_cpp_feature("time") + AP_begin_voltage = get_cpp_feature("AP_begin_voltage") stim_start_idx = numpy.flatnonzero(time >= stim_start)[0] stim_end_idx = numpy.flatnonzero(time >= stim_end)[0] @@ -380,8 +380,8 @@ def depol_block_bool(): def spikes_per_burst(): """Calculate the number of spikes per burst""" - burst_begin_indices = _get_cpp_feature("burst_begin_indices") - burst_end_indices = _get_cpp_feature("burst_end_indices") + burst_begin_indices = get_cpp_feature("burst_begin_indices") + burst_end_indices = get_cpp_feature("burst_end_indices") if burst_begin_indices is None: return None @@ -424,18 +424,23 @@ def spikes_in_burst1_burstlast_diff(): ]) -def _get_cpp_feature(feature_name): - """Get cpp feature""" +def get_cpp_feature(featureName, raise_warnings=None): + """Return value of feature implemented in cpp""" cppcoreFeatureValues = list() - exitCode = efel.cppcore.getFeature(feature_name, cppcoreFeatureValues) + exitCode = cppcore.getFeature(featureName, cppcoreFeatureValues) if exitCode < 0: + if raise_warnings: + import warnings + warnings.warn( + "Error while calculating feature %s: %s" % + (featureName, cppcore.getgError()), + RuntimeWarning) return None else: return numpy.array(cppcoreFeatureValues) -def _get_cpp_data(data_name): - """Get cpp data value""" - - return efel.cppcore.getMapDoubleData(data_name)[0] +def _get_cpp_data(data_name: str) -> float: + """Get cpp data value.""" + return cppcore.getMapDoubleData(data_name)[0] diff --git a/efel/units/__init__.py b/efel/units/__init__.py index a0433fc3..24ec54f4 100644 --- a/efel/units/__init__.py +++ b/efel/units/__init__.py @@ -4,6 +4,10 @@ _units_raw = pkgutil.get_data(__name__, "units.json") + +if _units_raw is None: + raise ValueError("Failed to load units.json") + _units = json.loads(_units_raw) diff --git a/tests/test_units.py b/tests/test_units.py index e8d6fd35..89eb863a 100644 --- a/tests/test_units.py +++ b/tests/test_units.py @@ -1,7 +1,9 @@ """Unit tests for units module.""" - +import importlib +import pytest from efel.units import get_unit +from unittest.mock import patch def test_get_unit(): @@ -11,3 +13,15 @@ def test_get_unit(): assert get_unit("AP1_amp") != "wrong unit" assert get_unit("AP1_amp") == "mV" assert get_unit("ohmic_input_resistance") == "MΩ" + + +@patch('efel.units.pkgutil.get_data') +def test_get_data_failure(mock_get_data): + """Test for handling failure in loading units.json.""" + mock_get_data.return_value = None + + with pytest.raises(ValueError) as excinfo: + # Dynamically reload the module to simulate the import with mock + importlib.reload(importlib.import_module('efel.units')) + + assert str(excinfo.value) == "Failed to load units.json" diff --git a/tests/testdata/allfeatures/convert.py b/tests/testdata/allfeatures/convert.py deleted file mode 100644 index 62566438..00000000 --- a/tests/testdata/allfeatures/convert.py +++ /dev/null @@ -1,19 +0,0 @@ -import json -import collections - -er = collections.OrderedDict() - -with open('expectedresults.txt', 'r') as er_file: - er_content = er_file.read() - -for line in er_content.split('\n'): - split_line = line.split() - if len(split_line) == 0: - continue - elif len(split_line) == 1: - er[split_line[0]] = None - else: - er[split_line[0]] = [str(x) for x in split_line[1:]] - -with open('expectedresults.json', 'w') as er_json: - json.dump(er, er_json, indent=4, separators=(',', ': ')) diff --git a/tox.ini b/tox.ini index 84866ecd..2922bdb9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = docs,py3-{test} +envlist = docs,lint,py3-{test} minversion = 4 [gh-actions] python = @@ -7,6 +7,7 @@ python = 3.9: py3 3.10: py3 3.11: py3 + 3.12: py3 [testenv] @@ -14,13 +15,11 @@ envdir = {toxworkdir}/py3-test deps = pytest>=7.3.1 scipy>=1.10.1 - pycodestyle>=2.11.0 pytest-xdist>=3.3.1 extras = neo usedevelop=True commands = - pycodestyle --ignore=E402,W503,W504 --exclude=_version.py --max-line-length=88 efel tests pytest -sx -n auto tests @@ -78,3 +77,12 @@ commands = make html SPHINXOPTS=-W # make sure the feature names and units are up-to-date pytest test_feature_units_in_docs.py + +[testenv:lint] +envdir = {toxworkdir}/lint +deps = + pycodestyle>=2.11.0 + mypy>=1.8.0 +commands = + pycodestyle --ignore=E402,W503,W504 --exclude=_version.py --max-line-length=88 efel tests + mypy efel tests --ignore-missing-imports