From 0bb1318d74a1512f5c6bd2625d4a979aa6cbf1c3 Mon Sep 17 00:00:00 2001 From: Yi-111-a <153097222+Yi-111-a@users.noreply.github.com> Date: Fri, 25 Sep 2026 13:05:01 +0800 Subject: [PATCH 1/2] Fix Neuralynx unclosed recording header parsing --- neo/rawio/neuralynxrawio/nlxheader.py | 3 ++- neo/test/rawiotest/test_neuralynxrawio.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/neo/rawio/neuralynxrawio/nlxheader.py b/neo/rawio/neuralynxrawio/nlxheader.py index 54bd57c99..04999f7ec 100644 --- a/neo/rawio/neuralynxrawio/nlxheader.py +++ b/neo/rawio/neuralynxrawio/nlxheader.py @@ -350,7 +350,8 @@ def _setTimeDate(self, txt_header): sr = NlxHeader._closeDatetime2_pat.search(txt_header) if sr: dt2 = sr.groupdict() - self["recording_closed"] = dateutil.parser.parse(f"{dt2['date']} {dt2['time']}") + if (dt2["date"], dt2["time"]) != ("File", "was"): + self["recording_closed"] = dateutil.parser.parse(f"{dt2['date']} {dt2['time']}") def _normalize_types(self): """ diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 0a1c90980..af69efa69 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -563,5 +563,16 @@ def test_filename_prop(self): self.assertEqual(r"C:\CheetahData\2000-01-01_00-00-00\CSC5.ncs", hdr["OriginalFileName"]) +class TestNlxHeaderParsing(unittest.TestCase): + def test_unclosed_recording(self): + header = NlxHeader.__new__(NlxHeader) + header._setTimeDate( + "## Time Opened: (m/d/y): 1/1/2000 (h:m:s.ms) 18:28:39.821\n" + "## Date Closed: (m/d/y): File At Time: was not properly closed" + ) + + self.assertIsNone(header.get("recording_closed")) + + if __name__ == "__main__": unittest.main() From b2769e504578a2c35e345d51c059a4d71fa5f3ed Mon Sep 17 00:00:00 2001 From: Yi-111-a <153097222+Yi-111-a@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:26:29 +0800 Subject: [PATCH 2/2] Warn instead of silently dropping the close time of an unclosed Neuralynx recording Folds the warning from #1908 into this branch, so the unclosed-recording case both leaves `recording_closed` unset and tells the user why, instead of parsing the sentinel text through dateutil. Co-Authored-By: Claude Opus 4.8 (1M context) --- neo/rawio/neuralynxrawio/nlxheader.py | 6 +++++- neo/test/rawiotest/test_neuralynxrawio.py | 9 +++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/neo/rawio/neuralynxrawio/nlxheader.py b/neo/rawio/neuralynxrawio/nlxheader.py index 04999f7ec..292ed8458 100644 --- a/neo/rawio/neuralynxrawio/nlxheader.py +++ b/neo/rawio/neuralynxrawio/nlxheader.py @@ -1,6 +1,7 @@ from packaging.version import Version import os import re +import warnings from collections import OrderedDict from neo.rawio.neuralynxrawio.ncssections import AcqType @@ -350,7 +351,10 @@ def _setTimeDate(self, txt_header): sr = NlxHeader._closeDatetime2_pat.search(txt_header) if sr: dt2 = sr.groupdict() - if (dt2["date"], dt2["time"]) != ("File", "was"): + if (dt2["date"], dt2["time"]) == ("File", "was"): + # Cheetah writes this in place of the close time, see #1901 + warnings.warn("Text header does not contain recording closed time. File was not closed properly.") + else: self["recording_closed"] = dateutil.parser.parse(f"{dt2['date']} {dt2['time']}") def _normalize_types(self): diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index af69efa69..4f2581e1d 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -566,10 +566,11 @@ def test_filename_prop(self): class TestNlxHeaderParsing(unittest.TestCase): def test_unclosed_recording(self): header = NlxHeader.__new__(NlxHeader) - header._setTimeDate( - "## Time Opened: (m/d/y): 1/1/2000 (h:m:s.ms) 18:28:39.821\n" - "## Date Closed: (m/d/y): File At Time: was not properly closed" - ) + with self.assertWarnsRegex(UserWarning, "File was not closed properly"): + header._setTimeDate( + "## Time Opened: (m/d/y): 1/1/2000 (h:m:s.ms) 18:28:39.821\n" + "## Date Closed: (m/d/y): File At Time: was not properly closed" + ) self.assertIsNone(header.get("recording_closed"))