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 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: 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",