Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions haystack/components/preprocessors/python_code_splitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
41 changes: 41 additions & 0 deletions test/components/preprocessors/test_python_code_splitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#
# SPDX-License-Identifier: Apache-2.0

import ast
import textwrap

import pytest
Expand Down Expand Up @@ -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
Expand Down