Skip to content

Commit ab3d0f5

Browse files
h-mayorquinzm711
andauthored
Fix NeuralynxRawIO reading in one-dir mode with nested directories (#1777)
* fix one-dir mode in neuralynx * Update neo/rawio/neuralynxrawio/neuralynxrawio.py Co-authored-by: Zach McKenzie <[email protected]> * Update neo/rawio/neuralynxrawio/neuralynxrawio.py Co-authored-by: Zach McKenzie <[email protected]> * sort by name and not by file path * pathlib --------- Co-authored-by: Zach McKenzie <[email protected]>
1 parent 43c6415 commit ab3d0f5

File tree

2 files changed

+73
-23
lines changed

2 files changed

+73
-23
lines changed

neo/rawio/neuralynxrawio/neuralynxrawio.py

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
)
5656
import numpy as np
5757
import os
58-
import pathlib
58+
from pathlib import Path
5959
import copy
6060
import warnings
6161
from collections import namedtuple, OrderedDict
@@ -151,15 +151,15 @@ def __init__(
151151

152152
if filename is not None:
153153
include_filenames = [filename]
154-
warnings.warn("`filename` is deprecated and will be removed. Please use `include_filenames` instead")
154+
warnings.warn("`filename` is deprecated and will be removed in version 1.0. Please use `include_filenames` instead")
155155

156156
if exclude_filename is not None:
157157
if isinstance(exclude_filename, str):
158158
exclude_filenames = [exclude_filename]
159159
else:
160160
exclude_filenames = exclude_filename
161161
warnings.warn(
162-
"`exclude_filename` is deprecated and will be removed. Please use `exclude_filenames` instead"
162+
"`exclude_filename` is deprecated and will be removed in version 1.0. Please use `exclude_filenames` instead"
163163
)
164164

165165
if include_filenames is None:
@@ -214,30 +214,43 @@ def _parse_header(self):
214214
unit_annotations = []
215215
event_annotations = []
216216

217-
if self.rawmode == "one-dir":
218-
filenames = sorted(os.listdir(self.dirname))
219-
else:
220-
filenames = self.include_filenames
221-
222-
filenames = [f for f in filenames if f not in self.exclude_filenames]
223-
full_filenames = [os.path.join(self.dirname, f) for f in filenames]
224-
225-
for filename in full_filenames:
226-
if not os.path.isfile(filename):
227-
raise ValueError(
228-
f"Provided Filename is not a file: "
229-
f"{filename}. If you want to provide a "
230-
f"directory use the `dirname` keyword"
231-
)
217+
# 1) Get file paths based on mode and validate existence for multiple-files mode
218+
if self.rawmode == "multiple-files":
219+
# For multiple-files mode, validate that all explicitly provided files exist
220+
file_paths = []
221+
for filename in self.include_filenames:
222+
full_path = Path(self.dirname) / filename
223+
if not full_path.is_file():
224+
raise ValueError(
225+
f"Provided Filename is not a file: "
226+
f"{full_path}. If you want to provide a "
227+
f"directory use the `dirname` keyword"
228+
)
229+
file_paths.append(full_path)
230+
else: # one-dir mode
231+
# For one-dir mode, get all files from directory
232+
dir_path = Path(self.dirname)
233+
file_paths = [p for p in dir_path.iterdir() if p.is_file()]
234+
file_paths = sorted(file_paths, key=lambda p: p.name)
235+
236+
# 2) Filter by exclude filenames
237+
file_paths = [fp for fp in file_paths if fp.name not in self.exclude_filenames]
238+
239+
# 3) Filter to keep only files with correct extensions
240+
# Note: suffix[1:] removes the leading dot from file extension (e.g., ".ncs" -> "ncs")
241+
valid_file_paths = [
242+
fp for fp in file_paths
243+
if fp.suffix[1:].lower() in self.extensions
244+
]
245+
246+
# Convert back to strings for backwards compatibility with existing code
247+
full_filenames = [str(fp) for fp in valid_file_paths]
232248

233249
stream_props = {} # {(sampling_rate, n_samples, t_start): {stream_id: [filenames]}
234250

235251
for filename in full_filenames:
236252
_, ext = os.path.splitext(filename)
237-
ext = ext[1:] # remove dot
238-
ext = ext.lower() # make lower case for comparisons
239-
if ext not in self.extensions:
240-
continue
253+
ext = ext[1:].lower() # remove dot and make lower case
241254

242255
# Skip Ncs files with only header. Other empty file types
243256
# will have an empty dataset constructed later.
@@ -574,7 +587,7 @@ def _get_file_map(filename):
574587
Create memory maps when needed
575588
see also https://github.com/numpy/numpy/issues/19340
576589
"""
577-
filename = pathlib.Path(filename)
590+
filename = Path(filename)
578591
suffix = filename.suffix.lower()[1:]
579592

580593
if suffix == "ncs":

neo/test/rawiotest/test_neuralynxrawio.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,43 @@ def test_exclude_filenames(self):
176176
self.assertEqual(len(rawio.header["spike_channels"]), 8)
177177
self.assertEqual(len(rawio.header["event_channels"]), 0)
178178

179+
def test_directory_in_data_folder(self):
180+
"""
181+
Test that directories inside the data folder are properly ignored
182+
and don't cause errors during parsing.
183+
"""
184+
import tempfile
185+
import shutil
186+
187+
# Use existing test data directory
188+
dname = self.get_local_path("neuralynx/Cheetah_v5.6.3/original_data/")
189+
190+
# Create a temporary copy to avoid modifying test data
191+
with tempfile.TemporaryDirectory() as temp_dir:
192+
temp_data_dir = os.path.join(temp_dir, "test_data")
193+
shutil.copytree(dname, temp_data_dir)
194+
195+
# Create a subdirectory inside the test data
196+
test_subdir = os.path.join(temp_data_dir, "raw fscv data with all recorded ch")
197+
os.makedirs(test_subdir, exist_ok=True)
198+
199+
# Create some files in the subdirectory to make it more realistic
200+
with open(os.path.join(test_subdir, "some_file.txt"), "w") as f:
201+
f.write("test file content")
202+
203+
# This should not raise an error despite the directory presence
204+
rawio = NeuralynxRawIO(dirname=temp_data_dir)
205+
rawio.parse_header()
206+
207+
# Verify that the reader still works correctly
208+
self.assertEqual(rawio._nb_segment, 2)
209+
self.assertEqual(len(rawio.ncs_filenames), 2)
210+
self.assertEqual(len(rawio.nev_filenames), 1)
211+
sigHdrs = rawio.header["signal_channels"]
212+
self.assertEqual(sigHdrs.size, 2)
213+
self.assertEqual(len(rawio.header["spike_channels"]), 8)
214+
self.assertEqual(len(rawio.header["event_channels"]), 2)
215+
179216

180217
class TestNcsRecordingType(BaseTestRawIO, unittest.TestCase):
181218
"""

0 commit comments

Comments
 (0)