diff --git a/eegdash/features/extractors.py b/eegdash/features/extractors.py index 5df28663..c0bb0759 100644 --- a/eegdash/features/extractors.py +++ b/eegdash/features/extractors.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from abc import ABC, abstractmethod from collections.abc import Callable from functools import partial diff --git a/eegdash/features/feature_bank/complexity.py b/eegdash/features/feature_bank/complexity.py index bd29f4c6..3aeef2fb 100644 --- a/eegdash/features/feature_bank/complexity.py +++ b/eegdash/features/feature_bank/complexity.py @@ -36,8 +36,12 @@ def preprocess(self, x, m=2, r=0.2, l=1): counts_m = np.empty((*x.shape[:-1], (x.shape[-1] - m + 1) // l)) counts_mp1 = np.empty((*x.shape[:-1], (x.shape[-1] - m) // l)) for i in np.ndindex(x.shape[:-1]): - counts_m[*i, :] = _channel_app_samp_entropy_counts(x[i], m, rr[i], l) - counts_mp1[*i, :] = _channel_app_samp_entropy_counts(x[i], m + 1, rr[i], l) + counts_m[i + (slice(None),)] = _channel_app_samp_entropy_counts( + x[i], m, rr[i], l + ) + counts_mp1[i + (slice(None),)] = _channel_app_samp_entropy_counts( + x[i], m + 1, rr[i], l + ) return counts_m, counts_mp1 @@ -62,7 +66,7 @@ def complexity_sample_entropy(counts_m, counts_mp1): def complexity_svd_entropy(x, m=10, tau=1): x_emb = np.empty((*x.shape[:-1], (x.shape[-1] - m + 1) // tau, m)) for i in np.ndindex(x.shape[:-1]): - x_emb[*i, :, :] = _create_embedding(x[i], m, tau) + x_emb[i + (slice(None), slice(None))] = _create_embedding(x[i], m, tau) s = np.linalg.svdvals(x_emb) s /= s.sum(axis=-1, keepdims=True) return -np.sum(s * np.log(s), axis=-1) diff --git a/eegdash/features/feature_bank/dimensionality.py b/eegdash/features/feature_bank/dimensionality.py index 0e8a535a..336744e4 100644 --- a/eegdash/features/feature_bank/dimensionality.py +++ b/eegdash/features/feature_bank/dimensionality.py @@ -26,7 +26,7 @@ def dimensionality_higuchi_fractal_dim(x, k_max=10, eps=1e-7): for i in np.ndindex(x.shape[:-1]): for k in range(1, k_max + 1): for m in range(k): - L_km[m] = np.mean(np.abs(np.diff(x[*i, m:], n=k))) + L_km[m] = np.mean(np.abs(np.diff(x[i + (slice(m, None),)], n=k))) L_k[k - 1] = (N - 1) * np.sum(L_km[:k]) / (k**3) L_k = np.maximum(L_k, eps) hfd[i] = np.linalg.lstsq(log_k, np.log(L_k))[0][0] diff --git a/eegdash/features/inspect.py b/eegdash/features/inspect.py index 8e379b58..395a3326 100644 --- a/eegdash/features/inspect.py +++ b/eegdash/features/inspect.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import inspect from collections.abc import Callable diff --git a/eegdash/features/serialization.py b/eegdash/features/serialization.py index 5aeb1787..75ebdc34 100644 --- a/eegdash/features/serialization.py +++ b/eegdash/features/serialization.py @@ -6,6 +6,8 @@ """ +from __future__ import annotations + from pathlib import Path import pandas as pd diff --git a/tests/test_features.py b/tests/test_features.py new file mode 100644 index 00000000..8a721ba1 --- /dev/null +++ b/tests/test_features.py @@ -0,0 +1,67 @@ +"""Test for features module Python 3.10+ compatibility.""" + +import pytest + + +def test_import_features_module(): + """Test that the features module can be imported without syntax errors. + + This test ensures Python 3.10+ compatibility by verifying that: + 1. Type annotations with list[], type[], and | syntax work (via __future__ imports) + 2. No Python 3.11+ exclusive syntax is used (like *unpacking in subscripts) + """ + try: + import eegdash.features + + assert eegdash.features is not None + except SyntaxError as e: + pytest.fail(f"SyntaxError when importing eegdash.features: {e}") + except ImportError as e: + pytest.fail(f"ImportError when importing eegdash.features: {e}") + + +def test_import_features_submodules(): + """Test that all features submodules can be imported.""" + submodules = [ + "eegdash.features.inspect", + "eegdash.features.extractors", + "eegdash.features.serialization", + "eegdash.features.datasets", + "eegdash.features.decorators", + "eegdash.features.feature_bank", + "eegdash.features.feature_bank.complexity", + "eegdash.features.feature_bank.dimensionality", + "eegdash.features.feature_bank.signal", + "eegdash.features.feature_bank.spectral", + "eegdash.features.feature_bank.connectivity", + "eegdash.features.feature_bank.csp", + ] + + for module_name in submodules: + try: + __import__(module_name) + except SyntaxError as e: + pytest.fail(f"SyntaxError when importing {module_name}: {e}") + except ImportError: + # Some imports might fail due to missing dependencies, that's ok + # We only care about SyntaxError + pass + + +def test_features_basic_functionality(): + """Test basic features module functionality.""" + from eegdash.features import ( + get_all_feature_extractors, + get_all_feature_kinds, + get_all_features, + ) + + # These should return lists without errors + features = get_all_features() + assert isinstance(features, list) + + extractors = get_all_feature_extractors() + assert isinstance(extractors, list) + + kinds = get_all_feature_kinds() + assert isinstance(kinds, list)