diff --git a/neo/rawio/cedrawio.py b/neo/rawio/cedrawio.py index e53cc190d..59619767c 100644 --- a/neo/rawio/cedrawio.py +++ b/neo/rawio/cedrawio.py @@ -19,6 +19,10 @@ Author : Samuel Garcia """ +import importlib +import importlib.util +from functools import cache + import numpy as np from .baserawio import ( @@ -31,6 +35,33 @@ ) +@cache +def _get_sonpy_namespace(): + """Return the sonpy namespace exposing SonFile, whatever the installed layout.""" + if importlib.util.find_spec("sonpy") is None: + raise ImportError("sonpy is not installed. Install it with `pip install sonpy`.") + + sonpy = importlib.import_module("sonpy") + + # <= 1.9.5 binds the extension module as an attribute, not a submodule, + # so find_spec("sonpy.lib") cannot see it. + lib = getattr(sonpy, "lib", None) + if lib is not None and hasattr(lib, "SonFile"): + return lib + + # >= 1.9.12 on Windows/macOS re-exports into the package namespace. + if hasattr(sonpy, "SonFile"): + return sonpy + + # >= 1.9.12 on Linux ships an empty __init__.py. + if importlib.util.find_spec("sonpy.sonpy") is not None: + nested = importlib.import_module("sonpy.sonpy") + if hasattr(nested, "SonFile"): + return nested + + raise ImportError("sonpy is installed but exposes no SonFile.") + + class CedRawIO(BaseRawIO): """ Class for reading data from CED (Cambridge Electronic Design) spike2. @@ -48,6 +79,13 @@ class CedRawIO(BaseRawIO): * This IO reads smr and smrx files + * sonpy is installed by the ``ced`` extra, but upstream only publishes wheels for Windows, + and for Linux and macOS from Python 3.14 onwards. Elsewhere the extra resolves to nothing + installable and this class raises an ImportError naming the constraint on first use; the + PyPI source distribution ships a Windows binary and is not usable. + + * Old smr files can be read without sonpy using Spike2RawIO. Only smrx requires this class. + """ extensions = ["smr", "smrx"] @@ -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",