Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 43 additions & 5 deletions neo/rawio/cedrawio.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
Author : Samuel Garcia
"""

import importlib
import importlib.util
from functools import cache

import numpy as np

from .baserawio import (
Expand All @@ -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.
Expand All @@ -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"]
Expand All @@ -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()
Expand All @@ -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:
Expand All @@ -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
Expand Down
15 changes: 4 additions & 11 deletions neo/test/iotest/test_cedio.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 4 additions & 1 deletion neo/test/rawiotest/test_cedrawio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down