From 9e1da97b386518397bd0e186c41b2bf78e20f2c0 Mon Sep 17 00:00:00 2001 From: "Axel.Cffrd.Dnty" <150222552+AxelNoun@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:52:29 +0200 Subject: [PATCH 1/3] 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 --- neo/rawio/cedrawio.py | 48 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/neo/rawio/cedrawio.py b/neo/rawio/cedrawio.py index e53cc190d..e94ec4f76 100644 --- a/neo/rawio/cedrawio.py +++ b/neo/rawio/cedrawio.py @@ -19,6 +19,9 @@ Author : Samuel Garcia """ +import functools +import importlib + import numpy as np from .baserawio import ( @@ -31,6 +34,41 @@ ) +@functools.cache +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`` shipped a single pure-Python wheel whose ``__init__.py`` + dispatched on the platform and bound the binary to ``lib``. + * ``>= 1.9.12`` ships one binary wheel per interpreter. The top level does + ``from .sonpy import *`` on Windows and macOS, but the Linux wheel ships + an empty ``__init__.py``, so only the ``sonpy.sonpy`` extension module is + populated there. + + Probing all three keeps old and new installations working. + """ + import sonpy + + candidates = [getattr(sonpy, "lib", None), sonpy] + try: + candidates.append(importlib.import_module("sonpy.sonpy")) + except ImportError: + pass + + for namespace in candidates: + if namespace is not None and hasattr(namespace, "SonFile"): + return namespace + + raise ImportError( + "The installed sonpy package exposes no 'SonFile' symbol " + "(tried sonpy.lib, sonpy and sonpy.sonpy). " + "Note that sonpy only publishes wheels for Windows, and for Linux and macOS " + "on Python 3.14 and above; the source distribution is not usable." + ) + + class CedRawIO(BaseRawIO): """ Class for reading data from CED (Cambridge Electronic Design) spike2. @@ -67,9 +105,9 @@ def _source_name(self): 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 +120,7 @@ def _parse_header(self): 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 +143,13 @@ def _parse_header(self): 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 From b7f8b1c8bad1f76b372c9a04deae326f5396c1d6 Mon Sep 17 00:00:00 2001 From: "Axel.Cffrd.Dnty" <150222552+AxelNoun@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:52:29 +0200 Subject: [PATCH 2/3] 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 --- neo/test/iotest/test_cedio.py | 15 ++++----------- neo/test/rawiotest/test_cedrawio.py | 5 ++++- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/neo/test/iotest/test_cedio.py b/neo/test/iotest/test_cedio.py index d47d45408..bbb956179 100644 --- a/neo/test/iotest/test_cedio.py +++ b/neo/test/iotest/test_cedio.py @@ -1,17 +1,10 @@ import unittest -from platform import system -from sys import maxsize try: - if system() == "Windows": - if maxsize > 2**32: - import sonpy.amd64.sonpy - else: - import sonpy.win32.sonpy - elif system() == "Darwin": - import sonpy.darwin.sonpy - elif system() == "Linux": - import sonpy.linux.sonpy + from neo.rawio.cedrawio import _get_sonpy_namespace + + # Raises ImportError if sonpy is missing or exposes no usable namespace. + _get_sonpy_namespace() from neo.io import CedIO except ImportError: HAVE_SONPY = False diff --git a/neo/test/rawiotest/test_cedrawio.py b/neo/test/rawiotest/test_cedrawio.py index f2dfde3b4..04923ed67 100644 --- a/neo/test/rawiotest/test_cedrawio.py +++ b/neo/test/rawiotest/test_cedrawio.py @@ -5,7 +5,10 @@ from neo.test.rawiotest.common_rawio_test import BaseTestRawIO try: - import sonpy + from neo.rawio.cedrawio import _get_sonpy_namespace + + # Raises ImportError if sonpy is missing or exposes no usable namespace. + _get_sonpy_namespace() HAVE_SONPY = True except ImportError: From 54d84dea8869ec9e538bb8e5087f8708cc7430ab Mon Sep 17 00:00:00 2001 From: "Axel.Cffrd.Dnty" <150222552+AxelNoun@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:52:29 +0200 Subject: [PATCH 3/3] 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 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d21d4181c..0cb04114e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ test = [ "coverage", "coveralls", "pillow", - "sonpy;python_version<'3.10'", + "sonpy; platform_system=='Windows' or python_version>='3.14'", "pynwb", "probeinterface", "zugbruecke>=0.2",