Skip to content

Commit 16877aa

Browse files
authored
Fix record name and add write_dir in csv2ann (#599)
Closes #595. - The record name is now `os.path.splitext(os.path.basename(file_name))[0]`, the same as in `csv_to_wfdb`. Before, `file_name.split(".")[0]` turned `./x.csv` into `""` and `data/v1.2/x.csv` into `data/v1`. - New `write_dir` argument (default `""`, the current directory), passed to `wrann`. It's added at the end of the signature so positional calls keep working. - Tests for time onsets, sample onsets without a header, and paths with dots (`./x.csv`, `data/v1.2/x.csv`). The two tests that exercise the fix fail on `main`. - `csv2ann` is added to `docs/convert.rst`. The docs build passes with `-W`. **Behaviour change:** the annotation file used to be written next to the CSV file (for paths without other dots). Now it's written to `write_dir`, like `csv_to_wfdb`. That follows the note in #595 that both functions should write to the same place. If you'd prefer to keep the old location as the default, I can make `write_dir=None` mean "next to the CSV file" instead. `ruff check` is clean and `pytest -m "not network"` passes (85 tests).
2 parents 4bee652 + 8ce6cd3 commit 16877aa

4 files changed

Lines changed: 67 additions & 11 deletions

File tree

‎docs/changes.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ Unreleased
2323
**Speed up XQRS backsearch**
2424
``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.
2525

26+
**Fix record name and add write directory in csv2ann**
27+
``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.
28+
2629
**Use Ruff for formatting and linting**
2730
The ``dev`` extra now includes ``ruff`` instead of ``black`` and ``pylint``, and the code is formatted with a line length of 88 (the default).
2831

‎docs/convert.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ CSV
88
---
99

1010
.. automodule:: wfdb.io.convert.csv
11-
:members: csv_to_wfdb
11+
:members: csv_to_wfdb, csv2ann
1212

1313
EDF
1414
---

‎src/wfdb/io/convert/csv.py‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ def csv2ann(
482482
header=True,
483483
delimiter=",",
484484
verbose=False,
485+
write_dir="",
485486
):
486487
"""Read a CSV file and return an `Annotation` object or write a file.
487488
@@ -490,11 +491,10 @@ def csv2ann(
490491
Parameters
491492
----------
492493
file_name : str
493-
The name of the CSV file to be read, including the '.csv' file
494-
extension. If the argument contains any path delimiter characters, the
495-
argument will be interpreted as PATH/BASE_RECORD. Both relative and
496-
absolute paths are accepted. The BASE_RECORD file name will be used to
497-
name the annotation file with the desired extension.
494+
The path to the CSV file to be read, including the '.csv' file
495+
extension. Both relative and absolute paths are accepted. The file
496+
name without its directory and extension is used as the record name
497+
of the annotation file.
498498
extension : str, optional
499499
The string annotation file extension.
500500
fs : float, optional
@@ -522,6 +522,9 @@ def csv2ann(
522522
verbose : bool, optional
523523
Whether to print all the information read about the file (True) or
524524
not (False).
525+
write_dir : str, optional
526+
The directory in which to write the annotation file. The default is
527+
the current working directory. Ignored if `record_only` is True.
525528
526529
Returns
527530
-------
@@ -627,8 +630,7 @@ def csv2ann(
627630
raise Exception("""The number of columns in the CSV was not
628631
recognized.""")
629632

630-
# Remove extension from input file name
631-
file_name = file_name.split(".")[0]
633+
record_name = os.path.splitext(os.path.basename(file_name))[0]
632634
if time_onset:
633635
if not fs:
634636
raise Exception("""`fs` must be provided if `time_onset` is True
@@ -652,7 +654,7 @@ def csv2ann(
652654
if verbose:
653655
print("Finished creating Annotation object")
654656
return Annotation(
655-
record_name=file_name,
657+
record_name=record_name,
656658
extension=extension,
657659
sample=sample,
658660
symbol=symbol,
@@ -664,7 +666,7 @@ def csv2ann(
664666
)
665667
else:
666668
wrann(
667-
file_name,
669+
record_name,
668670
extension,
669671
sample=sample,
670672
symbol=symbol,
@@ -673,6 +675,7 @@ def csv2ann(
673675
num=num,
674676
aux_note=aux_note,
675677
fs=fs,
678+
write_dir=write_dir,
676679
)
677680
if verbose:
678681
print("Finished writing Annotation file")

‎tests/io/test_convert.py‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import numpy as np
44

5-
from wfdb.io.convert.csv import csv_to_wfdb
5+
from wfdb.io.annotation import rdann
6+
from wfdb.io.convert.csv import csv2ann, csv_to_wfdb
67
from wfdb.io.convert.edf import read_edf
78
from wfdb.io.record import rdrecord
89

@@ -145,3 +146,52 @@ def test_write_dir(self, tmp_path):
145146
assert record_write.sig_name == record_100_dat.sig_name
146147
assert record_write.p_signal.size == 1300000
147148
assert record_write.p_signal.size == record_100_dat.p_signal.size
149+
150+
151+
class TestCsv2Ann:
152+
"""Tests for csv2ann."""
153+
154+
def test_time_onsets(self, tmp_path):
155+
"""Time onsets are converted to samples and written to write_dir."""
156+
csv_file = tmp_path / "in" / "example.csv"
157+
csv_file.parent.mkdir()
158+
csv_file.write_text("onset,description\n0.2,p-wave\n0.8,qrs\n")
159+
out_dir = tmp_path / "out"
160+
out_dir.mkdir()
161+
162+
csv2ann(str(csv_file), fs=360, write_dir=str(out_dir))
163+
164+
ann = rdann(str(out_dir / "example"), "atr")
165+
assert ann.sample.tolist() == [72, 288]
166+
assert ann.symbol == ['"', '"']
167+
assert ann.aux_note == ["p-wave", "qrs"]
168+
169+
def test_sample_onsets_no_header(self, tmp_path):
170+
"""Sample onsets are used as-is when time_onset is False."""
171+
csv_file = tmp_path / "example.csv"
172+
csv_file.write_text("5,p-wave\n13,qrs\n")
173+
174+
ann = csv2ann(
175+
str(csv_file), fs=10, time_onset=False, header=False, record_only=True
176+
)
177+
178+
assert ann.sample.tolist() == [5, 13]
179+
assert ann.aux_note == ["p-wave", "qrs"]
180+
181+
def test_record_name_with_dots(self, tmp_path, monkeypatch):
182+
"""Dots in the directory or a ./ prefix don't truncate the name (#595)."""
183+
data_dir = tmp_path / "data" / "v1.2"
184+
data_dir.mkdir(parents=True)
185+
(data_dir / "x.csv").write_text("onset,description\n0.5,qrs\n")
186+
monkeypatch.chdir(data_dir)
187+
188+
ann = csv2ann("./x.csv", fs=100, record_only=True)
189+
assert ann.record_name == "x"
190+
191+
ann = csv2ann(str(data_dir / "x.csv"), fs=100, record_only=True)
192+
assert ann.record_name == "x"
193+
194+
out_dir = tmp_path / "out"
195+
out_dir.mkdir()
196+
csv2ann(str(data_dir / "x.csv"), fs=100, write_dir=str(out_dir))
197+
assert (out_dir / "x.atr").exists()

0 commit comments

Comments
 (0)