Summary
neo/rawio/cedrawio.py reaches the sonpy API through sonpy.lib:
import sonpy
self.smrx_file = sonpy.lib.SonFile(sName=str(self.filename), bReadOnly=True)
sonpy 1.9.12 (released 2026-06-09) reorganised its package layout and no longer defines
lib. Every call site in cedrawio.py therefore raises:
AttributeError: module 'sonpy' has no attribute 'lib'
The compiled library itself is fine — it opens and reads .smrx files normally. Only the
namespace moved. SpikeInterface's read_ced is affected transitively.
Reproduction
uv venv --python 3.14 && uv pip install sonpy neo
import sonpy
print(sonpy.__version__ if hasattr(sonpy, "__version__") else "n/a")
sonpy.lib.SonFile # AttributeError: module 'sonpy' has no attribute 'lib'
from neo.rawio import CedRawIO
CedRawIO(filename="m365_1sec.smrx").parse_header() # same AttributeError
Evidence
Tested with neo 0.14.5 and spikeinterface 0.104.8.
| OS |
Python |
sonpy resolved |
wheel tag |
import sonpy |
sonpy.lib? |
CedRawIO.parse_header |
native read of a real .smrx |
| Windows |
3.10 – 3.14 |
1.9.12 |
cp3xx-win_amd64 |
OK |
absent |
AttributeError |
works |
| Linux x86_64 |
3.14 |
1.9.12 |
cp314-manylinux_2_39_x86_64 |
OK |
absent |
AttributeError |
not tested |
| Linux x86_64 |
3.13 |
1.9.5 (fallback) |
py3-none-any |
ImportError |
n/a |
n/a |
n/a |
| macOS |
— |
— |
— |
not tested |
not tested |
not tested |
not tested |
On Windows the file read succeeds through the new namespace: GetOpenError() == 0,
MaxChannels() == 101, GetVersion() == 257, plausible timebase. So this is purely an
import-path problem, not a functional regression in sonpy's reader.
The Linux 3.13 row is a different failure mode worth noting separately: no 1.9.12 wheel
exists for cp310–cp313 outside Windows, the resolver falls back to sonpy 1.9.5, and its
bundled sonpy/linux/sonpy.so targets the CPython 3.9 ABI —
ImportError: undefined symbol: _PyThreadState_UncheckedGet.
Root cause
sonpy's packaging changed shape between 1.9.5 and 1.9.12.
Every release from 1.1 through 1.9.5 shipped a single py3-none-any wheel with a 331-byte
__init__.py that dispatched per platform and bound the binary to lib:
if system() == 'Windows':
import sonpy.amd64.sonpy as lib
elif system() == 'Darwin':
import sonpy.darwin.sonpy as lib
elif system() == 'Linux':
import sonpy.linux.sonpy as lib
1.9.12 ships per-interpreter binary wheels instead, and lib is gone from all eight of them.
The replacement __init__.py is inconsistent across platforms in the same release:
| wheel |
__init__.py |
content |
cp39 – cp314 win_amd64 |
103 bytes |
docstring + from .sonpy import * + __version__ |
cp314 macosx universal2 |
21 bytes |
from .sonpy import * |
cp314 manylinux_2_39_x86_64 |
0 bytes |
empty |
The empty Linux __init__.py looks like a build-side mistake and is worth reporting to CED
separately, but it matters here: on Linux, neither sonpy.lib nor sonpy.SonFile resolves.
Only sonpy.sonpy.SonFile does. Any fix therefore needs three fallbacks, not two.
Why pinning sonpy < 1.9.12 will not work
The version metadata makes the two mutually exclusive:
neo 0.14.5 declares Requires-Python: >=3.10
sonpy 1.9.5 declares Requires-Python: >=3.9, <3.10
There is no sonpy release before 1.9.12 that is installable on any Python that Neo supports.
CED support has effectively been unreachable since Neo dropped 3.9; 1.9.12 made sonpy
installable again on modern Python, which is what surfaced the namespace mismatch.
Proposed fix
Resolve the namespace once, then use it at the three call sites. Full diff at the bottom
of this issue; it applies cleanly to main (35cbce7) and touches only cedrawio.py.
def _get_sonpy_namespace():
import importlib
import sonpy
for candidate in (
lambda: sonpy.lib, # <= 1.9.5
lambda: sonpy, # >= 1.9.12, Windows / macOS
lambda: importlib.import_module("sonpy.sonpy"), # >= 1.9.12, Linux
):
try:
namespace = candidate()
except (AttributeError, ImportError):
continue
if hasattr(namespace, "SonFile"):
return namespace
raise ImportError(...)
Verified on Linux / Python 3.14 / sonpy 1.9.12: the helper resolves to sonpy.sonpy,
SonFile, DataType.Adc, DataType.AdcMark and MarkerFilter are all reachable, and
CedRawIO.parse_header() runs to completion instead of raising.
The existing skip guard in neo/test/iotest/test_cedio.py also needs updating — it imports
sonpy.amd64.sonpy / sonpy.linux.sonpy, so HAVE_SONPY is silently False with 1.9.12
and the CED tests never run.
Not covered here
- macOS has not been tested. It is the one platform where the 1.9.12 wheel exists only for
cp314, so 3.10–3.13 would hit the fallback path instead. Happy to run it if someone has
a machine available.
- Whether a full read (not just
parse_header) returns identical data before and after the
change has not been checked on Linux for lack of a test file.
Side observation
CedRawIO.parse_header() completes without error on a nonexistent filename, yielding zero
channels — sonpy returns an error code rather than raising, and it is not checked. Minor and
unrelated, but possibly worth its own issue.
Patch
Against main (35cbce7). Only neo/rawio/cedrawio.py is modified.
--- a/neo/rawio/cedrawio.py
+++ b/neo/rawio/cedrawio.py
@@ -31,6 +31,42 @@
)
+def _get_sonpy_namespace():
+ """Return the sonpy module namespace that exposes ``SonFile``.
+
+ The layout of the sonpy package changed in 1.9.12:
+
+ * <= 1.9.5 ``sonpy/__init__.py`` binds the platform binary to ``lib``
+ * >= 1.9.12 the top level does ``from .sonpy import *`` (Windows, macOS),
+ but the Linux wheel ships an empty ``__init__.py``, so only the
+ ``sonpy.sonpy`` extension module is populated.
+
+ Probing all three keeps both old and new installs working.
+ """
+ import importlib
+
+ import sonpy
+
+ candidates = (
+ lambda: sonpy.lib,
+ lambda: sonpy,
+ lambda: importlib.import_module("sonpy.sonpy"),
+ )
+ for candidate in candidates:
+ try:
+ namespace = candidate()
+ except (AttributeError, ImportError):
+ continue
+ if hasattr(namespace, "SonFile"):
+ return namespace
+
+ raise ImportError(
+ "The installed sonpy package exposes no 'SonFile' symbol "
+ "(tried sonpy.lib, sonpy and sonpy.sonpy). "
+ "See https://pypi.org/project/sonpy/ for supported versions."
+ )
+
+
class CedRawIO(BaseRawIO):
"""
Class for reading data from CED (Cambridge Electronic Design) spike2.
@@ -67,9 +103,9 @@
return self.filename
def _parse_header(self):
- import sonpy
+ sonpy_ns = _get_sonpy_namespace()
- self.smrx_file = sonpy.lib.SonFile(sName=str(self.filename), bReadOnly=True)
+ self.smrx_file = sonpy_ns.SonFile(sName=str(self.filename), bReadOnly=True)
smrx = self.smrx_file
self._time_base = smrx.GetTimeBase()
@@ -82,7 +118,7 @@
for chan_ind in range(smrx.MaxChannels()):
chan_type = smrx.ChannelType(chan_ind)
chan_id = str(chan_ind)
- if chan_type == sonpy.lib.DataType.Adc:
+ if chan_type == sonpy_ns.DataType.Adc:
physical_chan = smrx.PhysicalChannel(chan_ind)
divide = smrx.ChannelDivide(chan_ind)
if self.take_ideal_sampling_rate:
@@ -105,13 +141,13 @@
buffer_id = ""
signal_channels.append((ch_name, chan_id, sr, dtype, units, gain, offset, stream_id, buffer_id))
- elif chan_type == sonpy.lib.DataType.AdcMark:
+ elif chan_type == sonpy_ns.DataType.AdcMark:
# spike and waveforms : only spike times is used here
ch_name = smrx.GetChannelTitle(chan_ind)
first_time = smrx.FirstTime(chan_ind, 0, max_time)
max_time = smrx.ChannelMaxTime(chan_ind)
divide = smrx.ChannelDivide(chan_ind)
- # here we don't use filter (sonpy.lib.MarkerFilter()) so we get all marker
+ # here we don't use filter (sonpy_ns.MarkerFilter()) so we get all marker
wave_marks = smrx.ReadWaveMarks(chan_ind, int(max_time / divide), 0, max_time)
# here we load in memory all spike once because the access is really slow
I have this ready as a PR if the approach looks right to you — I did not want to open one
before checking whether you would rather raise a clear error than probe namespaces, and
whether the test_cedio.py skip guard should move in the same change.
Summary
neo/rawio/cedrawio.pyreaches the sonpy API throughsonpy.lib:sonpy 1.9.12 (released 2026-06-09) reorganised its package layout and no longer defines
lib. Every call site incedrawio.pytherefore raises:The compiled library itself is fine — it opens and reads
.smrxfiles normally. Only thenamespace moved. SpikeInterface's
read_cedis affected transitively.Reproduction
uv venv --python 3.14 && uv pip install sonpy neoEvidence
Tested with
neo 0.14.5andspikeinterface 0.104.8.import sonpysonpy.lib?CedRawIO.parse_header.smrxcp3xx-win_amd64AttributeErrorcp314-manylinux_2_39_x86_64AttributeErrorpy3-none-anyImportErrorOn Windows the file read succeeds through the new namespace:
GetOpenError() == 0,MaxChannels() == 101,GetVersion() == 257, plausible timebase. So this is purely animport-path problem, not a functional regression in sonpy's reader.
The Linux 3.13 row is a different failure mode worth noting separately: no 1.9.12 wheel
exists for cp310–cp313 outside Windows, the resolver falls back to sonpy 1.9.5, and its
bundled
sonpy/linux/sonpy.sotargets the CPython 3.9 ABI —ImportError: undefined symbol: _PyThreadState_UncheckedGet.Root cause
sonpy's packaging changed shape between 1.9.5 and 1.9.12.
Every release from 1.1 through 1.9.5 shipped a single
py3-none-anywheel with a 331-byte__init__.pythat dispatched per platform and bound the binary tolib:1.9.12 ships per-interpreter binary wheels instead, and
libis gone from all eight of them.The replacement
__init__.pyis inconsistent across platforms in the same release:__init__.pycp39–cp314win_amd64from .sonpy import *+__version__cp314macosx universal2from .sonpy import *cp314manylinux_2_39_x86_64The empty Linux
__init__.pylooks like a build-side mistake and is worth reporting to CEDseparately, but it matters here: on Linux, neither
sonpy.libnorsonpy.SonFileresolves.Only
sonpy.sonpy.SonFiledoes. Any fix therefore needs three fallbacks, not two.Why pinning
sonpy < 1.9.12will not workThe version metadata makes the two mutually exclusive:
neo 0.14.5declaresRequires-Python: >=3.10sonpy 1.9.5declaresRequires-Python: >=3.9, <3.10There is no sonpy release before 1.9.12 that is installable on any Python that Neo supports.
CED support has effectively been unreachable since Neo dropped 3.9; 1.9.12 made sonpy
installable again on modern Python, which is what surfaced the namespace mismatch.
Proposed fix
Resolve the namespace once, then use it at the three call sites. Full diff at the bottom
of this issue; it applies cleanly to
main(35cbce7) and touches onlycedrawio.py.Verified on Linux / Python 3.14 / sonpy 1.9.12: the helper resolves to
sonpy.sonpy,SonFile,DataType.Adc,DataType.AdcMarkandMarkerFilterare all reachable, andCedRawIO.parse_header()runs to completion instead of raising.The existing skip guard in
neo/test/iotest/test_cedio.pyalso needs updating — it importssonpy.amd64.sonpy/sonpy.linux.sonpy, soHAVE_SONPYis silentlyFalsewith 1.9.12and the CED tests never run.
Not covered here
cp314, so 3.10–3.13 would hit the fallback path instead. Happy to run it if someone has
a machine available.
parse_header) returns identical data before and after thechange has not been checked on Linux for lack of a test file.
Side observation
CedRawIO.parse_header()completes without error on a nonexistent filename, yielding zerochannels — sonpy returns an error code rather than raising, and it is not checked. Minor and
unrelated, but possibly worth its own issue.
Patch
Against
main(35cbce7). Onlyneo/rawio/cedrawio.pyis modified.I have this ready as a PR if the approach looks right to you — I did not want to open one
before checking whether you would rather raise a clear error than probe namespaces, and
whether the
test_cedio.pyskip guard should move in the same change.