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
58 changes: 53 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 functools
import importlib
import importlib.util

import numpy as np

from .baserawio import (
Expand All @@ -31,6 +35,43 @@
)


@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

for namespace in (getattr(sonpy, "lib", None), sonpy):
if namespace is not None and hasattr(namespace, "SonFile"):
return namespace

# Only reached with the >= 1.9.12 Linux wheels, whose __init__.py is empty.
# find_spec imports the parent package, and raises if it is not a package,
# so guard on __path__ before probing the submodule.
if hasattr(sonpy, "__path__") and importlib.util.find_spec("sonpy.sonpy") is not None:
namespace = importlib.import_module("sonpy.sonpy")
if 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.
Expand All @@ -48,6 +89,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 +115,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 +130,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 +153,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