From c5fbd50190cd7b243f4dacfffdd172768852c408 Mon Sep 17 00:00:00 2001 From: Bharat Kaurav <117659673+TheNetherWatcher@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:40:34 +0530 Subject: [PATCH 1/3] Fix record name and add write_dir in csv2ann Take the record name from the CSV file name with os.path.splitext, as csv_to_wfdb does, so paths with dots no longer truncate it. Add a write_dir argument, tests, and list csv2ann in the API docs. --- docs/changes.rst | 3 +++ docs/convert.rst | 2 +- src/wfdb/io/convert/csv.py | 21 ++++++++------- tests/io/test_convert.py | 52 +++++++++++++++++++++++++++++++++++++- 4 files changed, 67 insertions(+), 11 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index f27c7d49..ca077d42 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -23,6 +23,9 @@ Unreleased **Speed up XQRS backsearch** ``XQRS`` no longer rechecks peaks that an earlier backsearch already rejected. Long stretches without QRS complexes (for example a disconnected lead) made detection take quadratic time, up to hours on long records. The detected QRS locations are unchanged. +**Fix record name and add write directory in `csv2ann`** + ``csv2ann`` now takes the record name from the CSV file name without its directory and extension, as ``csv_to_wfdb`` does, so paths containing dots (such as ``./x.csv`` or ``data/v1.2/x.csv``) no longer give a wrong name. The annotation file is written to the new ``write_dir`` argument (the current directory by default) instead of next to the CSV file. ``csv2ann`` is now included in the API documentation. + **Use Ruff for formatting and linting** The ``dev`` extra now includes ``ruff`` instead of ``black`` and ``pylint``, and the code is formatted with a line length of 88 (the default). diff --git a/docs/convert.rst b/docs/convert.rst index 1fa8f570..dafe2e79 100644 --- a/docs/convert.rst +++ b/docs/convert.rst @@ -8,7 +8,7 @@ CSV --- .. automodule:: wfdb.io.convert.csv - :members: csv_to_wfdb + :members: csv_to_wfdb, csv2ann EDF --- diff --git a/src/wfdb/io/convert/csv.py b/src/wfdb/io/convert/csv.py index 28d20e9f..52922f99 100644 --- a/src/wfdb/io/convert/csv.py +++ b/src/wfdb/io/convert/csv.py @@ -482,6 +482,7 @@ def csv2ann( header=True, delimiter=",", verbose=False, + write_dir="", ): """Read a CSV file and return an `Annotation` object or write a file. @@ -490,11 +491,10 @@ def csv2ann( Parameters ---------- file_name : str - The name of the CSV file to be read, including the '.csv' file - extension. If the argument contains any path delimiter characters, the - argument will be interpreted as PATH/BASE_RECORD. Both relative and - absolute paths are accepted. The BASE_RECORD file name will be used to - name the annotation file with the desired extension. + The path to the CSV file to be read, including the '.csv' file + extension. Both relative and absolute paths are accepted. The file + name without its directory and extension is used as the record name + of the annotation file. extension : str, optional The string annotation file extension. fs : float, optional @@ -522,6 +522,9 @@ def csv2ann( verbose : bool, optional Whether to print all the information read about the file (True) or not (False). + write_dir : str, optional + The directory in which to write the annotation file. The default is + the current working directory. Ignored if `record_only` is True. Returns ------- @@ -627,8 +630,7 @@ def csv2ann( raise Exception("""The number of columns in the CSV was not recognized.""") - # Remove extension from input file name - file_name = file_name.split(".")[0] + record_name = os.path.splitext(os.path.basename(file_name))[0] if time_onset: if not fs: raise Exception("""`fs` must be provided if `time_onset` is True @@ -652,7 +654,7 @@ def csv2ann( if verbose: print("Finished creating Annotation object") return Annotation( - record_name=file_name, + record_name=record_name, extension=extension, sample=sample, symbol=symbol, @@ -664,7 +666,7 @@ def csv2ann( ) else: wrann( - file_name, + record_name, extension, sample=sample, symbol=symbol, @@ -673,6 +675,7 @@ def csv2ann( num=num, aux_note=aux_note, fs=fs, + write_dir=write_dir, ) if verbose: print("Finished writing Annotation file") diff --git a/tests/io/test_convert.py b/tests/io/test_convert.py index f233f4c8..e820548a 100644 --- a/tests/io/test_convert.py +++ b/tests/io/test_convert.py @@ -2,7 +2,8 @@ import numpy as np -from wfdb.io.convert.csv import csv_to_wfdb +from wfdb.io.annotation import rdann +from wfdb.io.convert.csv import csv2ann, csv_to_wfdb from wfdb.io.convert.edf import read_edf from wfdb.io.record import rdrecord @@ -145,3 +146,52 @@ def test_write_dir(self, tmp_path): assert record_write.sig_name == record_100_dat.sig_name assert record_write.p_signal.size == 1300000 assert record_write.p_signal.size == record_100_dat.p_signal.size + + +class TestCsv2Ann: + """Tests for csv2ann.""" + + def test_time_onsets(self, tmp_path): + """Time onsets are converted to samples and written to write_dir.""" + csv_file = tmp_path / "in" / "example.csv" + csv_file.parent.mkdir() + csv_file.write_text("onset,description\n0.2,p-wave\n0.8,qrs\n") + out_dir = tmp_path / "out" + out_dir.mkdir() + + csv2ann(str(csv_file), fs=360, write_dir=str(out_dir)) + + ann = rdann(str(out_dir / "example"), "atr") + assert ann.sample.tolist() == [72, 288] + assert ann.symbol == ['"', '"'] + assert ann.aux_note == ["p-wave", "qrs"] + + def test_sample_onsets_no_header(self, tmp_path): + """Sample onsets are used as-is when time_onset is False.""" + csv_file = tmp_path / "example.csv" + csv_file.write_text("5,p-wave\n13,qrs\n") + + ann = csv2ann( + str(csv_file), fs=10, time_onset=False, header=False, record_only=True + ) + + assert ann.sample.tolist() == [5, 13] + assert ann.aux_note == ["p-wave", "qrs"] + + def test_record_name_with_dots(self, tmp_path, monkeypatch): + """Dots in the directory or a ./ prefix don't truncate the name (#595).""" + data_dir = tmp_path / "data" / "v1.2" + data_dir.mkdir(parents=True) + (data_dir / "x.csv").write_text("onset,description\n0.5,qrs\n") + monkeypatch.chdir(data_dir) + + ann = csv2ann("./x.csv", fs=100, record_only=True) + assert ann.record_name == "x" + + ann = csv2ann(str(data_dir / "x.csv"), fs=100, record_only=True) + assert ann.record_name == "x" + + out_dir = tmp_path / "out" + out_dir.mkdir() + csv2ann(str(data_dir / "x.csv"), fs=100, write_dir=str(out_dir)) + assert (out_dir / "x.atr").exists() From 8900545b99140dc47fd98289ea4bcfc7ac176d0d Mon Sep 17 00:00:00 2001 From: Bharat Kaurav <117659673+TheNetherWatcher@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:10:44 +0530 Subject: [PATCH 2/3] Update csv2ann changelog entry --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index ca077d42..d2efccac 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -24,7 +24,7 @@ Unreleased ``XQRS`` no longer rechecks peaks that an earlier backsearch already rejected. Long stretches without QRS complexes (for example a disconnected lead) made detection take quadratic time, up to hours on long records. The detected QRS locations are unchanged. **Fix record name and add write directory in `csv2ann`** - ``csv2ann`` now takes the record name from the CSV file name without its directory and extension, as ``csv_to_wfdb`` does, so paths containing dots (such as ``./x.csv`` or ``data/v1.2/x.csv``) no longer give a wrong name. The annotation file is written to the new ``write_dir`` argument (the current directory by default) instead of next to the CSV file. ``csv2ann`` is now included in the API documentation. + ``csv2ann`` now takes the record name from the CSV file name without its directory and extension, as ``csv_to_wfdb`` does. Paths with a directory (such as ``data/x.csv``) used to fail when writing, and ``./x.csv`` gave an empty name. The annotation file is written to the new ``write_dir`` argument, which defaults to the current directory. ``csv2ann`` is now included in the API documentation. **Use Ruff for formatting and linting** The ``dev`` extra now includes ``ruff`` instead of ``black`` and ``pylint``, and the code is formatted with a line length of 88 (the default). From 8ce6cd3c3075f21c731869addd6239e079dc3c74 Mon Sep 17 00:00:00 2001 From: Bharat Kaurav <117659673+TheNetherWatcher@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:22:51 +0530 Subject: [PATCH 3/3] Drop backticks from changelog heading --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index d2efccac..b13c3220 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -23,7 +23,7 @@ Unreleased **Speed up XQRS backsearch** ``XQRS`` no longer rechecks peaks that an earlier backsearch already rejected. Long stretches without QRS complexes (for example a disconnected lead) made detection take quadratic time, up to hours on long records. The detected QRS locations are unchanged. -**Fix record name and add write directory in `csv2ann`** +**Fix record name and add write directory in csv2ann** ``csv2ann`` now takes the record name from the CSV file name without its directory and extension, as ``csv_to_wfdb`` does. Paths with a directory (such as ``data/x.csv``) used to fail when writing, and ``./x.csv`` gave an empty name. The annotation file is written to the new ``write_dir`` argument, which defaults to the current directory. ``csv2ann`` is now included in the API documentation. **Use Ruff for formatting and linting**