Skip to content

Commit c3b8d10

Browse files
authored
Merge pull request #1190 from JuliaSprenger/enh/get_io
[IO] Extend utilities to find suitable IO for a dataset
2 parents c052168 + fdb2fca commit c3b8d10

20 files changed

+154
-51
lines changed

neo/io/__init__.py

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
Functions:
1111
1212
.. autofunction:: neo.io.get_io
13+
.. autofunction:: neo.io.list_candidate_ios
1314
1415
1516
Classes:
@@ -262,7 +263,8 @@
262263
263264
"""
264265

265-
import os.path
266+
import pathlib
267+
from collections import Counter
266268

267269
# try to import the neuroshare library.
268270
# if it is present, use the neuroshareapiio to load neuroshare files
@@ -350,7 +352,7 @@
350352
CedIO,
351353
EDFIO,
352354
ElanIO,
353-
# ElphyIO,
355+
ElphyIO,
354356
ExampleIO,
355357
IgorIO,
356358
IntanIO,
@@ -359,7 +361,8 @@
359361
MEArecIO,
360362
MaxwellIO,
361363
MicromedIO,
362-
NixIO, # place NixIO before other IOs that use HDF5 to make it the default for .h5 files
364+
NixIO,
365+
NixIOFr,
363366
NeoMatlabIO,
364367
NestIO,
365368
NeuralynxIO,
@@ -384,14 +387,86 @@
384387
WinWcpIO
385388
]
386389

390+
# for each supported extension list the ios supporting it
391+
io_by_extension = {}
392+
for current_io in iolist: # do not use `io` as variable name here as this overwrites the module io
393+
for extension in current_io.extensions:
394+
extension = extension.lower()
395+
# extension handling should not be case sensitive
396+
io_by_extension.setdefault(extension, []).append(current_io)
387397

388-
def get_io(filename, *args, **kwargs):
398+
399+
def get_io(file_or_folder, *args, **kwargs):
389400
"""
390401
Return a Neo IO instance, guessing the type based on the filename suffix.
391402
"""
392-
extension = os.path.splitext(filename)[1][1:]
393-
for io in iolist:
394-
if extension in io.extensions:
395-
return io(filename, *args, **kwargs)
403+
ios = list_candidate_ios(file_or_folder)
404+
for io in ios:
405+
try:
406+
return io(file_or_folder, *args, **kwargs)
407+
except:
408+
continue
409+
410+
raise IOError(f"Could not identify IO for {file_or_folder}")
411+
396412

397-
raise IOError("File extension %s not registered" % extension)
413+
def list_candidate_ios(file_or_folder, ignore_patterns=['*.ini', 'README.txt', 'README.md']):
414+
"""
415+
Identify neo IO that can potentially load data in the file or folder
416+
417+
Parameters
418+
----------
419+
file_or_folder (str, pathlib.Path)
420+
Path to the file or folder to load
421+
ignore_patterns (list)
422+
List of patterns to ignore when scanning for known formats. See pathlib.PurePath.match().
423+
Default: ['ini']
424+
425+
Returns
426+
-------
427+
list
428+
List of neo io classes that are associated with the file extensions detected
429+
"""
430+
file_or_folder = pathlib.Path(file_or_folder)
431+
432+
if file_or_folder.is_file():
433+
suffix = file_or_folder.suffix[1:].lower()
434+
if suffix not in io_by_extension:
435+
raise ValueError(f'{suffix} is not a supported format of any IO.')
436+
return io_by_extension[suffix]
437+
438+
elif file_or_folder.is_dir():
439+
# scan files in folder to determine io type
440+
filenames = [f for f in file_or_folder.glob('*') if f.is_file()]
441+
# keep only relevant filenames
442+
filenames = [f for f in filenames if f.suffix and not any([f.match(p) for p in ignore_patterns])]
443+
444+
# if no files are found in the folder, check subfolders
445+
# this is necessary for nested-folder based formats like spikeglx
446+
if not filenames:
447+
filenames = [f for f in file_or_folder.glob('**/*') if f.is_file()]
448+
# keep only relevant filenames
449+
filenames = [f for f in filenames if f.suffix and not any([f.match(p) for p in ignore_patterns])]
450+
451+
# if only file prefix was provided, e.g /mydatafolder/session1-
452+
# to select all files sharing the `session1-` prefix
453+
elif file_or_folder.parent.exists():
454+
filenames = file_or_folder.parent.glob(file_or_folder.name + '*')
455+
456+
else:
457+
raise ValueError(f'{file_or_folder} does not contain data files of a supported format')
458+
459+
# find the io that fits the best with the files contained in the folder
460+
potential_ios = []
461+
for filename in filenames:
462+
for suffix in filename.suffixes:
463+
suffix = suffix[1:].lower()
464+
if suffix in io_by_extension:
465+
potential_ios.extend(io_by_extension[suffix])
466+
467+
if not potential_ios:
468+
raise ValueError(f'Could not determine IO to load {file_or_folder}')
469+
470+
# return ios ordered by number of files supported
471+
counter = Counter(potential_ios).most_common()
472+
return [io for io, count in counter]

neo/io/neuroshareapiio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class NeuroshareapiIO(BaseIO):
7272

7373
name = "Neuroshare"
7474

75-
extensions = []
75+
extensions = ['mcd']
7676

7777
# This object operates on neuroshare files
7878
mode = "file"

neo/io/neurosharectypesio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ class NeurosharectypesIO(BaseIO):
108108
write_params = None
109109

110110
name = 'neuroshare'
111-
extensions = []
111+
extensions = ['mcd']
112112
mode = 'file'
113113

114114
def __init__(self, filename='', dllname=''):

neo/rawio/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@
235235

236236
def get_rawio_class(filename_or_dirname):
237237
"""
238-
Return a neo.rawio class guess from file extention.
238+
Return a neo.rawio class guess from file extension.
239239
"""
240240
_, ext = os.path.splitext(filename_or_dirname)
241241
ext = ext[1:]

neo/rawio/alphaomegarawio.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,18 @@ class AlphaOmegaRawIO(BaseRawIO):
9090
def __init__(self, dirname="", lsx_files=None, prune_channels=True):
9191
super().__init__(dirname=dirname)
9292
self.dirname = Path(dirname)
93+
9394
self._lsx_files = lsx_files
9495
self._mpx_files = None
95-
if self.dirname.is_dir():
96-
self._explore_folder()
97-
else:
98-
self.logger.error(f"{self.dirname} is not a folder")
9996
self._prune_channels = prune_channels
10097
self._opened_files = {}
10198
self._ignore_unknown_datablocks = True # internal debug property
10299

100+
if self.dirname.is_dir():
101+
self._explore_folder()
102+
else:
103+
raise ValueError(f"{self.dirname} is not a folder")
104+
103105
def _explore_folder(self):
104106
"""
105107
If class was instantiated with lsx_files (list of .lsx files), load only

neo/rawio/axographrawio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ class AxographRawIO(BaseRawIO):
218218
"""
219219
name = 'AxographRawIO'
220220
description = 'This IO reads .axgd/.axgx files created with AxoGraph'
221-
extensions = ['axgd', 'axgx']
221+
extensions = ['axgd', 'axgx', '']
222222
rawmode = 'one-file'
223223

224224
def __init__(self, filename, force_single_segment=False):

neo/rawio/biocamrawio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class BiocamRawIO(BaseRawIO):
2929
>>> float_chunk = r.rescale_signal_raw_to_float(raw_chunk, dtype='float64',
3030
channel_indexes=[0, 3, 6])
3131
"""
32-
extensions = ['h5']
32+
extensions = ['h5', 'brw']
3333
rawmode = 'one-file'
3434

3535
def __init__(self, filename=''):

neo/rawio/blackrockrawio.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ class BlackrockRawIO(BaseRawIO):
122122
"""
123123

124124
extensions = ['ns' + str(_) for _ in range(1, 7)]
125-
extensions.extend(['nev', ]) # 'sif', 'ccf' not yet supported
125+
extensions.extend(['nev', 'sif', 'ccf']) # 'sif', 'ccf' not yet supported
126126
rawmode = 'multi-file'
127127

128128
def __init__(self, filename=None, nsx_override=None, nev_override=None,
@@ -154,6 +154,9 @@ def __init__(self, filename=None, nsx_override=None, nev_override=None,
154154
else:
155155
self._filenames['nev'] = self.filename
156156

157+
self._filenames['sif'] = self.filename
158+
self._filenames['ccf'] = self.filename
159+
157160
# check which files are available
158161
self._avail_files = dict.fromkeys(self.extensions, False)
159162
self._avail_nsx = []

neo/rawio/neuralynxrawio/neuralynxrawio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ class NeuralynxRawIO(BaseRawIO):
7575
7676
Display all information about signal channels, units, segment size....
7777
"""
78-
extensions = ['nse', 'ncs', 'nev', 'ntt']
78+
extensions = ['nse', 'ncs', 'nev', 'ntt', 'nvt', 'nrd'] # nvt and nrd are not yet supported
7979
rawmode = 'one-dir'
8080

8181
_ncs_dtype = [('timestamp', 'uint64'), ('channel_id', 'uint32'), ('sample_rate', 'uint32'),

neo/rawio/nixrawio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
class NIXRawIO(BaseRawIO):
3333

34-
extensions = ['nix']
34+
extensions = ['nix', 'h5']
3535
rawmode = 'one-file'
3636

3737
def __init__(self, filename=''):

0 commit comments

Comments
 (0)