Skip to content

Commit a67a8de

Browse files
Fix CedRawIO with sonpy >= 1.9.12 and re-enable the CED tests (#1891)
* Fix CedRawIO with sonpy >= 1.9.12 sonpy 1.9.12 dropped the 'lib' namespace that cedrawio.py used, so every sonpy.lib.* access raised AttributeError. Resolve the namespace once, probing sonpy.lib, sonpy and sonpy.sonpy in turn: the last is needed on Linux, where the 1.9.12 wheel ships an empty __init__.py. Refs #1890 Co-authored-by: Cursor <cursoragent@cursor.com> * Use a single sonpy availability check in both CED tests test_cedio.py replicated the old per-platform sonpy dispatch and skipped with 1.9.12; test_cedrawio.py guarded on a bare 'import sonpy', which succeeds with 1.9.12 so the test would run and fail. Both now delegate to _get_sonpy_namespace(). Refs #1890 Co-authored-by: Cursor <cursoragent@cursor.com> * Install sonpy in CI where usable wheels exist The test extra declared sonpy;python_version<'3.10' while the project requires >=3.10, so sonpy was never installed and the CED tests never ran. Target the platform/version combinations sonpy actually publishes wheels for; the sdist ships a Windows .pyd and is not usable elsewhere. Refs #1890 Co-authored-by: Cursor <cursoragent@cursor.com> * Resolve the sonpy submodule with find_spec instead of try/except Per review: find_spec verifies sonpy.sonpy without importing it, so the try/except goes away. Two guards are needed for that to hold: importlib.util must be imported explicitly, and find_spec raises ModuleNotFoundError if sonpy is not a package, so check __path__ first. Probing after the first two candidates also means the submodule is only imported on Linux >= 1.9.12. Refs #1890 Co-authored-by: Cursor <cursoragent@cursor.com> * Document the sonpy platform constraint in the CedRawIO docstring sonpy only ships wheels for Windows, and for Linux and macOS from 3.14 on, so neo[ced] silently resolves to nothing elsewhere. Say so, and point users at Spike2RawIO for .smr files, which needs no sonpy. Refs #1890 Co-authored-by: Cursor <cursoragent@cursor.com> * Handle sonpy.lib as an attribute, not a submodule find_spec() cannot see sonpy.lib in <=1.9.5: __init__.py binds the extension module via 'import sonpy.<platform>.sonpy as lib', so there is no sonpy/lib.py for find_spec to find. Use getattr for that candidate and keep find_spec for the top-level guard and for the nested sonpy.sonpy of the 1.9.12 Linux wheel. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop @cache from _get_sonpy_namespace After the first call find_spec() and import_module() are sys.modules lookups, so the decorator saves ~1.2us on a path the import system already caches. It cannot help the one path with real cost either: functools.cache does not memoize exceptions, so find_spec()'s full sys.path scan on a missing sonpy re-runs on every call regardless. It also forces cache_clear() into any test of this function. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e1c90b6 commit a67a8de

4 files changed

Lines changed: 50 additions & 18 deletions

File tree

neo/rawio/cedrawio.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
Author : Samuel Garcia
2020
"""
2121

22+
import importlib
23+
import importlib.util
24+
2225
import numpy as np
2326

2427
from .baserawio import (
@@ -31,6 +34,32 @@
3134
)
3235

3336

37+
def _get_sonpy_namespace():
38+
"""Return the sonpy namespace exposing SonFile, whatever the installed layout."""
39+
if importlib.util.find_spec("sonpy") is None:
40+
raise ImportError("sonpy is not installed. Install it with `pip install sonpy`.")
41+
42+
sonpy = importlib.import_module("sonpy")
43+
44+
# <= 1.9.5 binds the extension module as an attribute, not a submodule,
45+
# so find_spec("sonpy.lib") cannot see it.
46+
lib = getattr(sonpy, "lib", None)
47+
if lib is not None and hasattr(lib, "SonFile"):
48+
return lib
49+
50+
# >= 1.9.12 on Windows/macOS re-exports into the package namespace.
51+
if hasattr(sonpy, "SonFile"):
52+
return sonpy
53+
54+
# >= 1.9.12 on Linux ships an empty __init__.py.
55+
if importlib.util.find_spec("sonpy.sonpy") is not None:
56+
nested = importlib.import_module("sonpy.sonpy")
57+
if hasattr(nested, "SonFile"):
58+
return nested
59+
60+
raise ImportError("sonpy is installed but exposes no SonFile.")
61+
62+
3463
class CedRawIO(BaseRawIO):
3564
"""
3665
Class for reading data from CED (Cambridge Electronic Design) spike2.
@@ -48,6 +77,13 @@ class CedRawIO(BaseRawIO):
4877
4978
* This IO reads smr and smrx files
5079
80+
* sonpy is installed by the ``ced`` extra, but upstream only publishes wheels for Windows,
81+
and for Linux and macOS from Python 3.14 onwards. Elsewhere the extra resolves to nothing
82+
installable and this class raises an ImportError naming the constraint on first use; the
83+
PyPI source distribution ships a Windows binary and is not usable.
84+
85+
* Old smr files can be read without sonpy using Spike2RawIO. Only smrx requires this class.
86+
5187
"""
5288

5389
extensions = ["smr", "smrx"]
@@ -67,9 +103,9 @@ def _source_name(self):
67103
return self.filename
68104

69105
def _parse_header(self):
70-
import sonpy
106+
sonpy_ns = _get_sonpy_namespace()
71107

72-
self.smrx_file = sonpy.lib.SonFile(sName=str(self.filename), bReadOnly=True)
108+
self.smrx_file = sonpy_ns.SonFile(sName=str(self.filename), bReadOnly=True)
73109
smrx = self.smrx_file
74110

75111
self._time_base = smrx.GetTimeBase()
@@ -82,7 +118,7 @@ def _parse_header(self):
82118
for chan_ind in range(smrx.MaxChannels()):
83119
chan_type = smrx.ChannelType(chan_ind)
84120
chan_id = str(chan_ind)
85-
if chan_type == sonpy.lib.DataType.Adc:
121+
if chan_type == sonpy_ns.DataType.Adc:
86122
physical_chan = smrx.PhysicalChannel(chan_ind)
87123
divide = smrx.ChannelDivide(chan_ind)
88124
if self.take_ideal_sampling_rate:
@@ -105,13 +141,13 @@ def _parse_header(self):
105141
buffer_id = ""
106142
signal_channels.append((ch_name, chan_id, sr, dtype, units, gain, offset, stream_id, buffer_id))
107143

108-
elif chan_type == sonpy.lib.DataType.AdcMark:
144+
elif chan_type == sonpy_ns.DataType.AdcMark:
109145
# spike and waveforms : only spike times is used here
110146
ch_name = smrx.GetChannelTitle(chan_ind)
111147
first_time = smrx.FirstTime(chan_ind, 0, max_time)
112148
max_time = smrx.ChannelMaxTime(chan_ind)
113149
divide = smrx.ChannelDivide(chan_ind)
114-
# here we don't use filter (sonpy.lib.MarkerFilter()) so we get all marker
150+
# here we don't use filter (sonpy_ns.MarkerFilter()) so we get all marker
115151
wave_marks = smrx.ReadWaveMarks(chan_ind, int(max_time / divide), 0, max_time)
116152

117153
# here we load in memory all spike once because the access is really slow

neo/test/iotest/test_cedio.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
11
import unittest
2-
from platform import system
3-
from sys import maxsize
42

53
try:
6-
if system() == "Windows":
7-
if maxsize > 2**32:
8-
import sonpy.amd64.sonpy
9-
else:
10-
import sonpy.win32.sonpy
11-
elif system() == "Darwin":
12-
import sonpy.darwin.sonpy
13-
elif system() == "Linux":
14-
import sonpy.linux.sonpy
4+
from neo.rawio.cedrawio import _get_sonpy_namespace
5+
6+
# Raises ImportError if sonpy is missing or exposes no usable namespace.
7+
_get_sonpy_namespace()
158
from neo.io import CedIO
169
except ImportError:
1710
HAVE_SONPY = False

neo/test/rawiotest/test_cedrawio.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
from neo.test.rawiotest.common_rawio_test import BaseTestRawIO
66

77
try:
8-
import sonpy
8+
from neo.rawio.cedrawio import _get_sonpy_namespace
9+
10+
# Raises ImportError if sonpy is missing or exposes no usable namespace.
11+
_get_sonpy_namespace()
912

1013
HAVE_SONPY = True
1114
except ImportError:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ test = [
6464
"coverage",
6565
"coveralls",
6666
"pillow",
67-
"sonpy;python_version<'3.10'",
67+
"sonpy; platform_system=='Windows' or python_version>='3.14'",
6868
"pynwb",
6969
"probeinterface",
7070
"zugbruecke>=0.2",

0 commit comments

Comments
 (0)