From f81ee063139d18a301d6ab5b2696719f0e22b186 Mon Sep 17 00:00:00 2001 From: Apoorv Darshan Date: Thu, 9 Jul 2026 22:10:12 +0530 Subject: [PATCH] Fix RST field list absorbing trailing blocks into last field In the ReST parser, the field-splitting regex only broke a field at the next `:field:` line or end of input. A trailing block after the field list (for example an `Example` section following `:rtype: dict`) has no leading `:`, so it was absorbed into the preceding field's value, leaving type_name as e.g. "dict\nExample:\n>>> ...". Terminate a field at a blank line followed by unindented, non-field content, which ends the RST field list. Indented continuation lines and unindented continuation lines without a blank separator are unaffected. Fixes #86 --- docstring_parser/rest.py | 9 +++++++- docstring_parser/tests/test_rest.py | 32 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/docstring_parser/rest.py b/docstring_parser/rest.py index 772cf2f..2dff710 100644 --- a/docstring_parser/rest.py +++ b/docstring_parser/rest.py @@ -127,8 +127,15 @@ def parse(text: T.Optional[str]) -> Docstring: types = {} rtypes = {} + # A field ends at the next field (``^:``), at the end of the chunk + # (``\Z``), or at a blank line followed by unindented, non-field + # content. The last case terminates the field list so that trailing + # blocks (e.g. an ``Example`` section) are not absorbed into the + # preceding field's value. for match in re.finditer( - r"(^:.*?)(?=^:|\Z)", meta_chunk, flags=re.S | re.M + r"(^:.*?)(?=^:|\n[ \t]*\n(?=[^ \t\n:])|\Z)", + meta_chunk, + flags=re.S | re.M, ): chunk = match.group(0) if not chunk: diff --git a/docstring_parser/tests/test_rest.py b/docstring_parser/tests/test_rest.py index 19ba3d8..ccfe870 100644 --- a/docstring_parser/tests/test_rest.py +++ b/docstring_parser/tests/test_rest.py @@ -378,6 +378,38 @@ def test_returns() -> None: assert docstring.many_returns == [docstring.returns] +def test_returns_does_not_absorb_trailing_block() -> None: + """Test that a trailing block after the field list is not absorbed. + + A blank line followed by unindented content terminates the field + list, so the last field's value must not swallow the rest of the + docstring (e.g. a trailing ``Example`` block). + """ + docstring = parse( + """ + Creates a user with the given username. + + :param username: The username of the user. + :type username: str + :return: A dictionary representing the created user. + :rtype: dict + + Example: + + >>> create_user("Alice", 25) + {'username': 'Alice'} + """ + ) + assert docstring.returns is not None + assert docstring.returns.type_name == "dict" + assert ( + docstring.returns.description + == "A dictionary representing the created user." + ) + assert docstring.many_returns[-1].type_name == "dict" + assert docstring.meta[-1].type_name == "dict" + + def test_yields() -> None: """Test parsing yields.""" docstring = parse(