From d25f606c843b1dea034e0cb086e66f9129fc701b Mon Sep 17 00:00:00 2001 From: Amir Fathi Date: Thu, 13 Aug 2026 07:02:56 +0000 Subject: [PATCH] fix: PythonCodeSplitter emits invalid code for docstring-only bodies --- .../preprocessors/python_code_splitter.py | 5 ++- ...-docstring-only-body-410e6c2a66da1bbf.yaml | 7 ++++ .../test_python_code_splitter.py | 41 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/python-code-splitter-docstring-only-body-410e6c2a66da1bbf.yaml diff --git a/haystack/components/preprocessors/python_code_splitter.py b/haystack/components/preprocessors/python_code_splitter.py index 92169c7fe0b..9dbaa361a2d 100644 --- a/haystack/components/preprocessors/python_code_splitter.py +++ b/haystack/components/preprocessors/python_code_splitter.py @@ -190,10 +190,11 @@ def _strip_docstring( return self._slice_lines(source_lines, unit_start, unit_end), None # Skip stripping when the docstring shares a line with the def/class (would - # leave broken syntax) or extends past the caller's slice (e.g. class_header). + # leave broken syntax), extends past the caller's slice (e.g. class_header), or + # is the body's only statement (stripping it would leave no body at all). ds_start = first.lineno ds_end = first.end_lineno or first.lineno - if ds_start <= node.lineno or ds_end > unit_end: + if len(body) == 1 or ds_start <= node.lineno or ds_end > unit_end: return self._slice_lines(source_lines, unit_start, unit_end), None before = source_lines[unit_start - 1 : ds_start - 1] diff --git a/releasenotes/notes/python-code-splitter-docstring-only-body-410e6c2a66da1bbf.yaml b/releasenotes/notes/python-code-splitter-docstring-only-body-410e6c2a66da1bbf.yaml new file mode 100644 index 00000000000..e84194e699e --- /dev/null +++ b/releasenotes/notes/python-code-splitter-docstring-only-body-410e6c2a66da1bbf.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fix ``PythonCodeSplitter`` with ``strip_docstrings=True`` emitting a syntactically + invalid chunk for any function, method, or class whose body consists solely of a + docstring (for example a one-line custom exception class). Such units are now left + unstripped instead of being reduced to a header with no body. diff --git a/test/components/preprocessors/test_python_code_splitter.py b/test/components/preprocessors/test_python_code_splitter.py index 267cb7d82cc..a9997f24fd9 100644 --- a/test/components/preprocessors/test_python_code_splitter.py +++ b/test/components/preprocessors/test_python_code_splitter.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 +import ast import textwrap import pytest @@ -687,6 +688,46 @@ def method(self): assert "Class-level docstring." not in (header.content or "") assert "Class-level docstring." in " | ".join(header.meta.get("docstrings") or []) + @pytest.mark.parametrize( + "source", + [ + pytest.param( + textwrap.dedent( + ''' + class MyError(Exception): + """Custom error raised when the widget explodes.""" + ''' + ).lstrip(), + id="docstring_only_class", + ), + pytest.param( + textwrap.dedent( + ''' + def foo(): + """This function intentionally does nothing yet.""" + ''' + ).lstrip(), + id="docstring_only_function", + ), + pytest.param( + textwrap.dedent( + ''' + class Widget: + def explode(self): + """Explode the widget.""" + ''' + ).lstrip(), + id="docstring_only_method", + ), + ], + ) + def test_strip_docstrings_skips_body_that_is_only_a_docstring(self, source): + """A def/class whose sole body statement is the docstring must not be stripped down to an empty body.""" + splitter = PythonCodeSplitter(min_effective_lines=1, max_effective_lines=10, strip_docstrings=True) + result = splitter.run(documents=[Document(content=source)]) + for chunk in result["documents"]: + ast.parse(chunk.content) # raises SyntaxError if the body was stripped down to nothing + class TestTopLevelStatements: @pytest.fixture