From 31a118b8f77a5949157fba8db55b5afdb2b1e4e2 Mon Sep 17 00:00:00 2001 From: Mohammad Hijjawi Date: Thu, 17 Sep 2026 11:58:16 +0100 Subject: [PATCH 1/3] fix: treat None TextCleaner inputs as empty strings TextCleaner applied regex, lower, and translate to every item. A None entry raised AttributeError. Co-authored-by: Cursor --- haystack/components/preprocessors/text_cleaner.py | 1 + .../notes/fix-text-cleaner-none-texts-d3a91c47e8b206f5.yaml | 5 +++++ test/components/preprocessors/test_text_cleaner.py | 6 ++++++ 3 files changed, 12 insertions(+) create mode 100644 releasenotes/notes/fix-text-cleaner-none-texts-d3a91c47e8b206f5.yaml diff --git a/haystack/components/preprocessors/text_cleaner.py b/haystack/components/preprocessors/text_cleaner.py index 39372d8d0f6..1e9a298803d 100644 --- a/haystack/components/preprocessors/text_cleaner.py +++ b/haystack/components/preprocessors/text_cleaner.py @@ -85,6 +85,7 @@ def run(self, texts: list[str]) -> dict[str, Any]: :returns: A dictionary with the following key: - `texts`: the cleaned list of strings. """ + texts = [text if isinstance(text, str) else "" for text in texts] if self._regex: texts = [self._regex.sub("", text) for text in texts] diff --git a/releasenotes/notes/fix-text-cleaner-none-texts-d3a91c47e8b206f5.yaml b/releasenotes/notes/fix-text-cleaner-none-texts-d3a91c47e8b206f5.yaml new file mode 100644 index 00000000000..b9aaa95034d --- /dev/null +++ b/releasenotes/notes/fix-text-cleaner-none-texts-d3a91c47e8b206f5.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + ``TextCleaner`` now treats ``None`` and non-string items as empty strings instead of raising + ``AttributeError`` when applying regex, case, or punctuation cleanup. diff --git a/test/components/preprocessors/test_text_cleaner.py b/test/components/preprocessors/test_text_cleaner.py index 0d9e9ae079e..9698820e49b 100644 --- a/test/components/preprocessors/test_text_cleaner.py +++ b/test/components/preprocessors/test_text_cleaner.py @@ -31,6 +31,12 @@ def test_run_with_empty_inputs(): assert result["texts"] == [] +def test_run_with_none_and_non_string_inputs(): + cleaner = TextCleaner(convert_to_lowercase=True, remove_punctuation=True) + result = cleaner.run(texts=["Hello, World!", None, 42, ""]) # type: ignore[list-item] + assert result["texts"] == ["hello world", "", "", ""] + + def test_run_with_regex(): cleaner = TextCleaner(remove_regexps=[r"\d+"]) result = cleaner.run(texts=["Open123 Source", "HaystackAI"]) From 28aa4ce1cc7baa81236f7df85f9c09047a3515fb Mon Sep 17 00:00:00 2001 From: Mohammad Hijjawi Date: Thu, 24 Sep 2026 13:54:51 +0100 Subject: [PATCH 2/3] Raise TypeError for non-string TextCleaner inputs Keep the list[str] contract: validate every element is a str and raise a clear TypeError instead of coercing None or other types. --- haystack/components/preprocessors/text_cleaner.py | 5 ++++- .../fix-text-cleaner-none-texts-d3a91c47e8b206f5.yaml | 4 ++-- test/components/preprocessors/test_text_cleaner.py | 9 ++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/haystack/components/preprocessors/text_cleaner.py b/haystack/components/preprocessors/text_cleaner.py index 1e9a298803d..7100ab225fc 100644 --- a/haystack/components/preprocessors/text_cleaner.py +++ b/haystack/components/preprocessors/text_cleaner.py @@ -84,8 +84,11 @@ def run(self, texts: list[str]) -> dict[str, Any]: :param texts: List of strings to clean. :returns: A dictionary with the following key: - `texts`: the cleaned list of strings. + :raises TypeError: + If any element of ``texts`` is not a ``str``. """ - texts = [text if isinstance(text, str) else "" for text in texts] + if not all(isinstance(text, str) for text in texts): + raise TypeError("`texts` must be a list of strings.") if self._regex: texts = [self._regex.sub("", text) for text in texts] diff --git a/releasenotes/notes/fix-text-cleaner-none-texts-d3a91c47e8b206f5.yaml b/releasenotes/notes/fix-text-cleaner-none-texts-d3a91c47e8b206f5.yaml index b9aaa95034d..a4b8eb0e5b2 100644 --- a/releasenotes/notes/fix-text-cleaner-none-texts-d3a91c47e8b206f5.yaml +++ b/releasenotes/notes/fix-text-cleaner-none-texts-d3a91c47e8b206f5.yaml @@ -1,5 +1,5 @@ --- fixes: - | - ``TextCleaner`` now treats ``None`` and non-string items as empty strings instead of raising - ``AttributeError`` when applying regex, case, or punctuation cleanup. + ``TextCleaner.run`` now raises a clear ``TypeError`` when any element of ``texts`` is not a + ``str``, instead of failing later with ``AttributeError`` during cleanup. diff --git a/test/components/preprocessors/test_text_cleaner.py b/test/components/preprocessors/test_text_cleaner.py index 9698820e49b..78b93decded 100644 --- a/test/components/preprocessors/test_text_cleaner.py +++ b/test/components/preprocessors/test_text_cleaner.py @@ -31,10 +31,13 @@ def test_run_with_empty_inputs(): assert result["texts"] == [] -def test_run_with_none_and_non_string_inputs(): +def test_run_rejects_non_string_inputs(): cleaner = TextCleaner(convert_to_lowercase=True, remove_punctuation=True) - result = cleaner.run(texts=["Hello, World!", None, 42, ""]) # type: ignore[list-item] - assert result["texts"] == ["hello world", "", "", ""] + try: + cleaner.run(texts=["Hello, World!", None, 42, ""]) # type: ignore[list-item] + assert False, "expected TypeError" + except TypeError as e: + assert "list of strings" in str(e) def test_run_with_regex(): From 19fbb8b418208bab4b7cb851822fc3587cd7a5c3 Mon Sep 17 00:00:00 2001 From: anakin87 Date: Thu, 24 Sep 2026 16:37:42 +0200 Subject: [PATCH 3/3] improvements --- haystack/components/preprocessors/text_cleaner.py | 4 ++-- ...-text-cleaner-none-texts-d3a91c47e8b206f5.yaml | 6 +++--- .../components/preprocessors/test_text_cleaner.py | 15 +++++++++------ 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/haystack/components/preprocessors/text_cleaner.py b/haystack/components/preprocessors/text_cleaner.py index 7100ab225fc..d77021678ae 100644 --- a/haystack/components/preprocessors/text_cleaner.py +++ b/haystack/components/preprocessors/text_cleaner.py @@ -85,9 +85,9 @@ def run(self, texts: list[str]) -> dict[str, Any]: :returns: A dictionary with the following key: - `texts`: the cleaned list of strings. :raises TypeError: - If any element of ``texts`` is not a ``str``. + If `texts` is not a list or any element is not a string. """ - if not all(isinstance(text, str) for text in texts): + if not isinstance(texts, list) or not all(isinstance(text, str) for text in texts): raise TypeError("`texts` must be a list of strings.") if self._regex: diff --git a/releasenotes/notes/fix-text-cleaner-none-texts-d3a91c47e8b206f5.yaml b/releasenotes/notes/fix-text-cleaner-none-texts-d3a91c47e8b206f5.yaml index a4b8eb0e5b2..34808f12de0 100644 --- a/releasenotes/notes/fix-text-cleaner-none-texts-d3a91c47e8b206f5.yaml +++ b/releasenotes/notes/fix-text-cleaner-none-texts-d3a91c47e8b206f5.yaml @@ -1,5 +1,5 @@ --- -fixes: +enhancements: - | - ``TextCleaner.run`` now raises a clear ``TypeError`` when any element of ``texts`` is not a - ``str``, instead of failing later with ``AttributeError`` during cleanup. + ``TextCleaner.run`` now raises a clear ``TypeError`` when ``texts`` is not a list or any element is not a ``str``, + instead of failing later or producing unexpected results. diff --git a/test/components/preprocessors/test_text_cleaner.py b/test/components/preprocessors/test_text_cleaner.py index 78b93decded..f71e088d7c8 100644 --- a/test/components/preprocessors/test_text_cleaner.py +++ b/test/components/preprocessors/test_text_cleaner.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 +import pytest + from haystack import Pipeline from haystack.components.preprocessors import TextCleaner @@ -31,13 +33,14 @@ def test_run_with_empty_inputs(): assert result["texts"] == [] -def test_run_rejects_non_string_inputs(): - cleaner = TextCleaner(convert_to_lowercase=True, remove_punctuation=True) - try: +def test_run_rejects_invalid_inputs(): + cleaner = TextCleaner() + + with pytest.raises(TypeError): cleaner.run(texts=["Hello, World!", None, 42, ""]) # type: ignore[list-item] - assert False, "expected TypeError" - except TypeError as e: - assert "list of strings" in str(e) + + with pytest.raises(TypeError): + cleaner.run(texts=15) # type: ignore[arg-type] def test_run_with_regex():