From df9ccd72c622bd25a5e342c5436695985d9df81e Mon Sep 17 00:00:00 2001 From: WEN Hao Date: Mon, 2 Nov 2026 16:47:32 +0800 Subject: [PATCH 1/2] fix: pandas 3 crash in get_contained_labels (set_index on string .values) Under pandas >= 3 (Arrow-backed strings), DataFrame.set_index rejects the ArrowStringArray produced by .values, so get_contained_labels raises TypeError whenever the annotation carries symbol or description. Pass the column names instead, with drop=False so the symbol/description column survives in the returned contained_labels (the old array-index path kept every column; plain set_index("symbol") would consume it). This is #575 rebased onto the restructured main: main has since moved to the src layout and independently converted all set_index(inplace=True) call sites to reassignments, so the remaining diff is exactly this two-line fix. Verified on pandas 3.1.0: rdann -> get_contained_labels -> set_label_elements emits zero Pandas4Warning and the contained_labels schema is unchanged. --- src/wfdb/io/annotation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wfdb/io/annotation.py b/src/wfdb/io/annotation.py index 6c3e3b0f..5914785c 100644 --- a/src/wfdb/io/annotation.py +++ b/src/wfdb/io/annotation.py @@ -1350,12 +1350,12 @@ def get_contained_labels(self, inplace=True): counts = np.unique(self.label_store, return_counts=True) elif self.symbol is not None: index_vals = set(self.symbol) - label_map = label_map.set_index(label_map["symbol"].values) + label_map = label_map.set_index("symbol", drop=False) reset_index = True counts = np.unique(self.symbol, return_counts=True) elif self.description is not None: index_vals = set(self.description) - label_map = label_map.set_index(label_map["description"].values) + label_map = label_map.set_index("description", drop=False) reset_index = True counts = np.unique(self.description, return_counts=True) else: From 5f2ed6a8e247665dab9d06fe0655f574958a1b79 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Mon, 2 Nov 2026 09:32:07 -0500 Subject: [PATCH 2/2] Add test and changelog entry for get_contained_labels with pandas 3 No test called get_contained_labels, so the crash with pandas 3 was not caught. The test reads an annotation with symbols only, as rdann returns by default, and fails on pandas 3 without the fix. --- changelog.d/contained-labels-pandas3.rst | 2 ++ tests/test_annotation.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 changelog.d/contained-labels-pandas3.rst diff --git a/changelog.d/contained-labels-pandas3.rst b/changelog.d/contained-labels-pandas3.rst new file mode 100644 index 00000000..f717d39e --- /dev/null +++ b/changelog.d/contained-labels-pandas3.rst @@ -0,0 +1,2 @@ +**Fix get_contained_labels with pandas 3** + ``Annotation.get_contained_labels`` no longer raises a ``TypeError`` with pandas 3 for annotations without ``label_store`` values, such as those read with ``rdann`` by default. diff --git a/tests/test_annotation.py b/tests/test_annotation.py index a0e901fd..37a9a8b6 100644 --- a/tests/test_annotation.py +++ b/tests/test_annotation.py @@ -342,3 +342,18 @@ def test_aux_note_none(temp_path): open(os.path.join(temp_path, "empty.atr"), "rb") as f2, ): assert f1.read() == f2.read() + + +def test_contained_labels_from_symbols(): + """Contained labels can be found from symbols alone""" + ann = wfdb.rdann("sample-data/100", "atr") + assert ann.label_store is None + labels = ann.get_contained_labels(inplace=False) + assert list(labels.columns) == [ + "label_store", + "symbol", + "description", + "n_occurrences", + ] + assert labels.loc[1, "symbol"] == "N" + assert labels["n_occurrences"].sum() == len(ann.sample)