From 16b84f877c057bf1799c0d870160436c50bcba33 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Sat, 14 Mar 2026 22:24:33 -0400 Subject: [PATCH 01/10] Add support for specifying default value in type declaration for numpydoc. Fix regex for default specified in description. Fix composition test for parameters docstring. Add unit tests that test regex matching independently --- docstring_parser/numpydoc.py | 40 ++++-- docstring_parser/tests/test_numpydoc.py | 184 ++++++++++++++++++++---- 2 files changed, 184 insertions(+), 40 deletions(-) diff --git a/docstring_parser/numpydoc.py b/docstring_parser/numpydoc.py index eca5233..1bab3e3 100644 --- a/docstring_parser/numpydoc.py +++ b/docstring_parser/numpydoc.py @@ -39,10 +39,22 @@ def _clean_str(string: str) -> T.Optional[str]: PARAM_KEY_REGEX = re.compile(r"^(?P.*?)(?:\s*:\s*(?P.*?))?$") PARAM_OPTIONAL_REGEX = re.compile(r"(?P.*?)(?:, optional|\(optional\))$") -# numpydoc format has no formal grammar for this, -# but we can make some educated guesses... +# Ideally, default value will be specified in the type declaration, +# for which the following are supported: +# +# copy : bool, default True +# copy : bool, default=True +# copy : bool, default: True +# PARAM_DEFAULT_REGEX = re.compile( - r"(?[\w\-\.]*\w)" + r"(?P.*?)(?:, default|\(default\))(?: | |=| = |= |: |)*(?P.*)$" # pylint: disable=C0301 +) + +# If the default value isn't specified in the type declaration, +# it might be in the description. There isn't any formal grammar for this +# in numpydoc, but we can make some educated guesses. +PARAM_DEFAULT_REGEX_IN_DESC = re.compile( + r"(?(?:['\"]).*?(?:['\"])|[\w\-\.]*\w)" # pylint: disable=C0301 ) RETURN_KEY_REGEX = re.compile(r"^(?:(?P.*?)\s*:\s*)?(?P.*?)$") @@ -131,7 +143,7 @@ class ParamSection(_KVSection): def _parse_item(self, key: str, value: str) -> DocstringParam: match = PARAM_KEY_REGEX.match(key) - arg_name = type_name = is_optional = None + arg_name = type_name = is_optional = default = None if match is not None: arg_name = match.group("name") type_name = match.group("type") @@ -143,9 +155,16 @@ def _parse_item(self, key: str, value: str) -> DocstringParam: else: is_optional = False - default = None - if len(value) > 0: - default_match = PARAM_DEFAULT_REGEX.search(value) + default_match = PARAM_DEFAULT_REGEX.match(type_name) + if default_match is not None: + is_optional = True + type_name = default_match.group("type") + default = default_match.group("value") + + # If the default wasn't specifified in the type declaration, + # try and see if we can find it in the description. + if len(value) > 0 and default is None: + default_match = PARAM_DEFAULT_REGEX_IN_DESC.search(value) if default_match is not None: default = default_match.group("value") @@ -409,8 +428,11 @@ def process_one( elif not head: head = "" - if isinstance(one, DocstringParam) and one.is_optional: - head += ", optional" + if isinstance(one, DocstringParam): + if one.is_optional and one.default is not None: + head += f", default={one.default}" + elif one.is_optional: + head += ", optional" if one.description: body = f"\n{indent}".join([head] + one.description.splitlines()) diff --git a/docstring_parser/tests/test_numpydoc.py b/docstring_parser/tests/test_numpydoc.py index 2f2ef42..907d2fb 100644 --- a/docstring_parser/tests/test_numpydoc.py +++ b/docstring_parser/tests/test_numpydoc.py @@ -3,7 +3,92 @@ import typing as T import pytest -from docstring_parser.numpydoc import compose, parse +from docstring_parser.numpydoc import ( + PARAM_DEFAULT_REGEX, + PARAM_DEFAULT_REGEX_IN_DESC, + PARAM_KEY_REGEX, + PARAM_OPTIONAL_REGEX, + compose, + parse, +) + + +@pytest.mark.parametrize( + "input_str, expected_name, expected_type", + [ + ("arg_name", "arg_name", None), + ("arg_name : type", "arg_name", "type"), + ("arg_name : type, optional", "arg_name", "type, optional"), + ("arg_name : type, default=10", "arg_name", "type, default=10"), + ], +) +def test_param_key_regex(input_str, expected_name, expected_type): + """Test parsing parameter keys.""" + match = PARAM_KEY_REGEX.match(input_str) + assert match is not None + assert match.group("name") == expected_name + assert match.group("type") == expected_type + + +@pytest.mark.parametrize( + "input_str, expected_match", + [ + ("", False), + ("type", False), + ("type, optional", True), + ], +) +def test_param_optional_regex(input_str, expected_match): + """Test parsing parameter optionality.""" + match = PARAM_OPTIONAL_REGEX.match(input_str) + if expected_match: + assert match is not None + else: + assert match is None + + +@pytest.mark.parametrize( + "input_str, expected_match, default_value", + [ + ("", False, None), + ("type", False, None), + ("type, default=10", True, "10"), + ("type, default='hello'", True, "'hello'"), + ('type, default="world"', True, '"world"'), + ("type, default: 1.5", True, "1.5"), + ("type, default 1.5", True, "1.5"), + ], +) +def test_param_default_regex(input_str, expected_match, default_value): + """Test parsing parameter default values.""" + match = PARAM_DEFAULT_REGEX.match(input_str) + if expected_match: + assert match is not None + assert match.group("type") == "type" + assert match.group("value") == default_value + else: + assert match is None + + +@pytest.mark.parametrize( + "input_str, expected_match, default_value", + [ + ("", False, None), + ("description without default", False, None), + ("description with default, default=10", True, "10"), + ("description with default, Default: 'hello'", True, "'hello'"), + ("description with default, defaults to 'world'", True, "'world'"), + ("description with default, defaults to 1.5", True, "1.5"), + ], +) +def test_param_default_regex_in_desc(input_str, expected_match, default_value): + """Test parsing parameter default values in descriptions.""" + match = PARAM_DEFAULT_REGEX_IN_DESC.search(input_str) + if expected_match: + assert match is not None + assert match.group("value") == default_value + else: + assert match is None @pytest.mark.parametrize( @@ -269,7 +354,40 @@ def test_meta_with_multiline_description() -> None: """ Parameters ---------- - arg4 : Optional[Dict[str, Any]], optional + arg3 : float, default=1.0 + The third arg. + """, + True, + "float", + "1.0", + ), + ( + """ + Parameters + ---------- + arg4: int, default 1 + The fourth arg. + """, + True, + "int", + "1", + ), + ( + """ + Parameters + ---------- + arg5: str, default: 'hello' + The fifth arg. + """, + True, + "str", + "'hello'", + ), + ( + """ + Parameters + ---------- + arg6 : Optional[Dict[str, Any]], optional The fourth arg. Defaults to None """, True, @@ -280,7 +398,7 @@ def test_meta_with_multiline_description() -> None: """ Parameters ---------- - arg5 : str, optional + arg7 : str, optional The fifth arg. Default: DEFAULT_ARGS """, True, @@ -1014,35 +1132,38 @@ def test_deprecation( """ Short description - Parameters: - ----------- - name - description 1 - priority: int - description 2 - sender: str, optional - description 3 - message: str, optional - description 4, defaults to 'hello' - multiline: str, optional - long description 5, - defaults to 'bye' + Parameters + ---------- + name + description 1 + priority: int + description 2 + sender: str, optional + description 3 + message: str, optional + description 4, defaults to 'hello' + multiline: str, optional + long description 5, + defaults to 'bye' + default_arg: str, default=10 + description 6, defaults to 10 """, - "Short description\n" - "\n" - "Parameters:\n" - "-----------\n" - " name\n" - " description 1\n" - " priority: int\n" - " description 2\n" - " sender: str, optional\n" - " description 3\n" - " message: str, optional\n" - " description 4, defaults to 'hello'\n" - " multiline: str, optional\n" - " long description 5,\n" - " defaults to 'bye'", + "Short description\n\n\n" + "Parameters\n" + "----------\n" + "name\n" + " description 1\n" + "priority : int\n" + " description 2\n" + "sender : str, optional\n" + " description 3\n" + "message : str, default='hello'\n" + " description 4, defaults to 'hello'\n" + "multiline : str, default='bye'\n" + " long description 5,\n" + " defaults to 'bye'\n" + "default_arg : str, default=10\n" + " description 6, defaults to 10", ), ( """ @@ -1088,4 +1209,5 @@ def test_deprecation( ) def test_compose(source: str, expected: str) -> None: """Test compose in default mode.""" + assert compose(parse(source)) == expected From 6496338ded718659f9da97982c5015c8d36c393e Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Sat, 14 Mar 2026 22:31:18 -0400 Subject: [PATCH 02/10] Add simple assertion to test_compose for numpydoc to check that the parsed test string docstring has DocstringMeta obj if expected --- docstring_parser/tests/test_numpydoc.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docstring_parser/tests/test_numpydoc.py b/docstring_parser/tests/test_numpydoc.py index 907d2fb..3bc2141 100644 --- a/docstring_parser/tests/test_numpydoc.py +++ b/docstring_parser/tests/test_numpydoc.py @@ -1210,4 +1210,13 @@ def test_deprecation( def test_compose(source: str, expected: str) -> None: """Test compose in default mode.""" - assert compose(parse(source)) == expected + docstring = parse(source) + + # We want to make sure that parse is correctly parsing the docstring and + # not returning the whole thing as a description. + if "-" in source: + assert len(docstring.meta) > 0 + else: + assert len(docstring.meta) == 0 + + assert compose(docstring) == expected From 4ff7007a4a2633f003af1a644bb544ea11275d4c Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Sat, 14 Mar 2026 22:49:19 -0400 Subject: [PATCH 03/10] Fix test_compose test case for numpydoc --- docstring_parser/tests/test_numpydoc.py | 223 ++++++++++++------------ 1 file changed, 113 insertions(+), 110 deletions(-) diff --git a/docstring_parser/tests/test_numpydoc.py b/docstring_parser/tests/test_numpydoc.py index 3bc2141..e4a1e74 100644 --- a/docstring_parser/tests/test_numpydoc.py +++ b/docstring_parser/tests/test_numpydoc.py @@ -4,10 +4,13 @@ import pytest from docstring_parser.numpydoc import ( + DEFAULT_SECTIONS, PARAM_DEFAULT_REGEX, PARAM_DEFAULT_REGEX_IN_DESC, PARAM_KEY_REGEX, PARAM_OPTIONAL_REGEX, + NumpydocParser, + Section, compose, parse, ) @@ -1002,41 +1005,41 @@ def test_deprecation( ( """ Short description - Meta: - ----- - asd + Meta + ---- + asd """, - "Short description\nMeta:\n-----\n asd", + "Short description\n\nMeta\n----\nasd", ), ( """ Short description Long description - Meta: - ----- - asd + Meta + ---- + asd """, "Short description\n" - "Long description\n" - "Meta:\n" - "-----\n" - " asd", + "Long description\n\n" + "Meta\n" + "----\n" + "asd", ), ( """ Short description First line Second line - Meta: - ----- - asd + Meta + ---- + asd """, "Short description\n" "First line\n" - " Second line\n" - "Meta:\n" - "-----\n" - " asd", + " Second line\n\n" + "Meta\n" + "----\n" + "asd", ), ( """ @@ -1044,17 +1047,17 @@ def test_deprecation( First line Second line - Meta: - ----- - asd + Meta + ---- + asd """, "Short description\n" "\n" "First line\n" - " Second line\n" - "Meta:\n" - "-----\n" - " asd", + " Second line\n\n" + "Meta\n" + "----\n" + "asd", ), ( """ @@ -1062,71 +1065,67 @@ def test_deprecation( First line Second line - - Meta: - ----- - asd + Meta + ---- + asd """, "Short description\n" "\n" "First line\n" - " Second line\n" - "\n" - "Meta:\n" - "-----\n" - " asd", + " Second line\n\n" + "Meta\n" + "----\n" + "asd", ), ( """ Short description - Meta: - ----- - asd - 1 - 2 - 3 + Meta + ---- + asd + 1 + 2 + 3 """, - "Short description\n" - "\n" - "Meta:\n" - "-----\n" - " asd\n" - " 1\n" - " 2\n" - " 3", + "Short description\n\n\n" + "Meta\n" + "----\n" + "asd\n" + " 1\n" + " 2\n" + " 3", ), ( """ Short description - Meta1: - ------ - asd - 1 - 2 - 3 - Meta2: - ------ - herp - Meta3: - ------ - derp + Meta1 + ----- + asd + 1 + 2 + 3 + Meta2 + ----- + herp + Meta3 + ----- + derp """, - "Short description\n" - "\n" - "Meta1:\n" - "------\n" - " asd\n" - " 1\n" - " 2\n" - " 3\n" - "Meta2:\n" - "------\n" - " herp\n" - "Meta3:\n" - "------\n" - " derp", + "Short description\n\n" + "Meta1\n" + "-----\n" + "asd\n" + "1\n" + " 2\n" + "3\n\n" + "Meta2\n" + "-----\n" + "herp\n\n" + "Meta3\n" + "-----\n" + "derp", ), ( """ @@ -1168,49 +1167,53 @@ def test_deprecation( ( """ Short description - Raises: - ------- - ValueError - description - """, - "Short description\n" - "Raises:\n" - "-------\n" - " ValueError\n" - " description", - ), - ( - """ - Description - Examples: - -------- - >>> test1a - >>> test1b - desc1a - desc1b - >>> test2a - >>> test2b - desc2a - desc2b + Raises + ------ + ValueError + description """, - "Description\n" - "Examples:\n" - "--------\n" - ">>> test1a\n" - ">>> test1b\n" - "desc1a\n" - "desc1b\n" - ">>> test2a\n" - ">>> test2b\n" - "desc2a\n" - "desc2b", + "Short description\n\n" + "Raises\n" + "------\n" + "ValueError\n" + " description", ), + # Parsing examples meta does not currently work correctly, so skip this case for now. + # ( + # """ + # Description + # Examples + # -------- + # >>> test1a + # >>> test1b + # desc1a + # desc1b + # >>> test2a + # >>> test2b + # desc2a + # desc2b + # """, + # "Description\n\n" + # "Examples\n" + # "--------\n" + # ">>> test1a\n" + # ">>> test1b\n" + # "desc1a\n" + # "desc1b\n" + # ">>> test2a\n" + # ">>> test2b\n" + # "desc2a\n" + # "desc2b", + # ), ], ) def test_compose(source: str, expected: str) -> None: """Test compose in default mode.""" - docstring = parse(source) + # Test cases use `Meta` meta, which is not included in the default sections. + addtl_sections = [Section(f"Meta{i if i > 1 else ''}", f"meta{i if i > 1 else ''}") for i in range(0, 4)] + parser_w_meta_section = NumpydocParser(sections = (DEFAULT_SECTIONS + addtl_sections)) + docstring = parser_w_meta_section.parse(source) # We want to make sure that parse is correctly parsing the docstring and # not returning the whole thing as a description. From 90abd7b2821b52dd70ac08f46bfb7d00dc944794 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Sat, 14 Mar 2026 22:50:36 -0400 Subject: [PATCH 04/10] Line length fixes from pre-commit --- docstring_parser/tests/test_numpydoc.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docstring_parser/tests/test_numpydoc.py b/docstring_parser/tests/test_numpydoc.py index e4a1e74..ea62cda 100644 --- a/docstring_parser/tests/test_numpydoc.py +++ b/docstring_parser/tests/test_numpydoc.py @@ -1178,7 +1178,8 @@ def test_deprecation( "ValueError\n" " description", ), - # Parsing examples meta does not currently work correctly, so skip this case for now. + # Parsing examples meta does not currently work correctly, + # so skip this case for now. # ( # """ # Description @@ -1210,9 +1211,14 @@ def test_deprecation( def test_compose(source: str, expected: str) -> None: """Test compose in default mode.""" - # Test cases use `Meta` meta, which is not included in the default sections. - addtl_sections = [Section(f"Meta{i if i > 1 else ''}", f"meta{i if i > 1 else ''}") for i in range(0, 4)] - parser_w_meta_section = NumpydocParser(sections = (DEFAULT_SECTIONS + addtl_sections)) + # Test cases use `Meta#`, which are not included in the default sections. + addtl_sections = [ + Section(f"Meta{i if i > 1 else ''}", f"meta{i if i > 1 else ''}") + for i in range(0, 4) + ] + parser_w_meta_section = NumpydocParser( + sections=(DEFAULT_SECTIONS + addtl_sections) + ) docstring = parser_w_meta_section.parse(source) # We want to make sure that parse is correctly parsing the docstring and From 33942dc7ea3e54a0c1384555545b1e6ee1600f10 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Sat, 14 Mar 2026 23:26:36 -0400 Subject: [PATCH 05/10] Correctly handle example meta in numpydoc compose --- docstring_parser/numpydoc.py | 9 +++++ docstring_parser/tests/test_numpydoc.py | 53 +++++++++++++------------ 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/docstring_parser/numpydoc.py b/docstring_parser/numpydoc.py index 1bab3e3..dc9cbbb 100644 --- a/docstring_parser/numpydoc.py +++ b/docstring_parser/numpydoc.py @@ -532,6 +532,14 @@ def process_sect(name: str, args: T.List[T.Any]): [item for item in docstring.raises or [] if item.args[0] == "warns"], ) + if len(docstring.examples) > 0: + parts.append("") + parts.append("Examples") + parts.append("--------") + for example in docstring.examples: + parts.append(example.snippet) + parts.append(example.description) + for meta in docstring.meta: if isinstance( meta, @@ -540,6 +548,7 @@ def process_sect(name: str, args: T.List[T.Any]): DocstringParam, DocstringReturns, DocstringRaises, + DocstringExample, ), ): continue # Already handled diff --git a/docstring_parser/tests/test_numpydoc.py b/docstring_parser/tests/test_numpydoc.py index ea62cda..16d0738 100644 --- a/docstring_parser/tests/test_numpydoc.py +++ b/docstring_parser/tests/test_numpydoc.py @@ -1180,32 +1180,32 @@ def test_deprecation( ), # Parsing examples meta does not currently work correctly, # so skip this case for now. - # ( - # """ - # Description - # Examples - # -------- - # >>> test1a - # >>> test1b - # desc1a - # desc1b - # >>> test2a - # >>> test2b - # desc2a - # desc2b - # """, - # "Description\n\n" - # "Examples\n" - # "--------\n" - # ">>> test1a\n" - # ">>> test1b\n" - # "desc1a\n" - # "desc1b\n" - # ">>> test2a\n" - # ">>> test2b\n" - # "desc2a\n" - # "desc2b", - # ), + ( + """ + Description + Examples + -------- + >>> test1a + >>> test1b + desc1a + desc1b + >>> test2a + >>> test2b + desc2a + desc2b + """, + "Description\n\n" + "Examples\n" + "--------\n" + ">>> test1a\n" + ">>> test1b\n" + "desc1a\n" + "desc1b\n" + ">>> test2a\n" + ">>> test2b\n" + "desc2a\n" + "desc2b", + ), ], ) def test_compose(source: str, expected: str) -> None: @@ -1227,5 +1227,6 @@ def test_compose(source: str, expected: str) -> None: assert len(docstring.meta) > 0 else: assert len(docstring.meta) == 0 + print([example.description for example in docstring.examples]) assert compose(docstring) == expected From 0cb1f558a585c6e4b2519ad2ccec69026505216d Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Sat, 14 Mar 2026 23:31:29 -0400 Subject: [PATCH 06/10] One extra test case for default value in description --- docstring_parser/tests/test_numpydoc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docstring_parser/tests/test_numpydoc.py b/docstring_parser/tests/test_numpydoc.py index 16d0738..89eaa97 100644 --- a/docstring_parser/tests/test_numpydoc.py +++ b/docstring_parser/tests/test_numpydoc.py @@ -82,6 +82,7 @@ def test_param_default_regex(input_str, expected_match, default_value): ("description with default, Default: 'hello'", True, "'hello'"), ("description with default, defaults to 'world'", True, "'world'"), ("description with default, defaults to 1.5", True, "1.5"), + ("description with default, default is 1.5", True, "1.5"), ], ) def test_param_default_regex_in_desc(input_str, expected_match, default_value): From 2f4d08761a561b1b1b5ac042a2e087d58ebc7719 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Sat, 14 Mar 2026 23:38:58 -0400 Subject: [PATCH 07/10] Add comment, use if default value is in compose --- docstring_parser/numpydoc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docstring_parser/numpydoc.py b/docstring_parser/numpydoc.py index dc9cbbb..d1abc40 100644 --- a/docstring_parser/numpydoc.py +++ b/docstring_parser/numpydoc.py @@ -428,8 +428,11 @@ def process_one( elif not head: head = "" + # If this is a parameter, check if it's optional. + # If it is and there's a not-None default, include that in the type + # declaration, otherwise just mark it as optional. if isinstance(one, DocstringParam): - if one.is_optional and one.default is not None: + if one.is_optional and one.default not in [None, "None"]: head += f", default={one.default}" elif one.is_optional: head += ", optional" From 82e0c3982c2a5b076a87de5c4f607deb7e44df45 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Mon, 16 Mar 2026 09:20:25 -0400 Subject: [PATCH 08/10] Remove comment/print leftover from debugging --- docstring_parser/tests/test_numpydoc.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/docstring_parser/tests/test_numpydoc.py b/docstring_parser/tests/test_numpydoc.py index 89eaa97..14d85b2 100644 --- a/docstring_parser/tests/test_numpydoc.py +++ b/docstring_parser/tests/test_numpydoc.py @@ -1179,8 +1179,6 @@ def test_deprecation( "ValueError\n" " description", ), - # Parsing examples meta does not currently work correctly, - # so skip this case for now. ( """ Description @@ -1228,6 +1226,5 @@ def test_compose(source: str, expected: str) -> None: assert len(docstring.meta) > 0 else: assert len(docstring.meta) == 0 - print([example.description for example in docstring.examples]) assert compose(docstring) == expected From d95d9274cb87af8caaa5cf79152155efdfdc4aa8 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Mon, 16 Mar 2026 19:45:53 -0400 Subject: [PATCH 09/10] Address comments from review. --- docstring_parser/numpydoc.py | 10 +++--- docstring_parser/tests/test_numpydoc.py | 46 ++++++++++++++++--------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/docstring_parser/numpydoc.py b/docstring_parser/numpydoc.py index d1abc40..7965b68 100644 --- a/docstring_parser/numpydoc.py +++ b/docstring_parser/numpydoc.py @@ -432,9 +432,9 @@ def process_one( # If it is and there's a not-None default, include that in the type # declaration, otherwise just mark it as optional. if isinstance(one, DocstringParam): - if one.is_optional and one.default not in [None, "None"]: + if one.default not in [None, "None"]: head += f", default={one.default}" - elif one.is_optional: + elif one.is_optional or one.default == "None": head += ", optional" if one.description: @@ -540,8 +540,10 @@ def process_sect(name: str, args: T.List[T.Any]): parts.append("Examples") parts.append("--------") for example in docstring.examples: - parts.append(example.snippet) - parts.append(example.description) + if example.snippet: + parts.append(example.snippet) + if example.description: + parts.append(example.description) for meta in docstring.meta: if isinstance( diff --git a/docstring_parser/tests/test_numpydoc.py b/docstring_parser/tests/test_numpydoc.py index 14d85b2..29bf23d 100644 --- a/docstring_parser/tests/test_numpydoc.py +++ b/docstring_parser/tests/test_numpydoc.py @@ -952,16 +952,17 @@ def test_deprecation( @pytest.mark.parametrize( - "source, expected", + "source, expected, expected_num_metas", [ - ("", ""), - ("\n", ""), - ("Short description", "Short description"), - ("\nShort description\n", "Short description"), - ("\n Short description\n", "Short description"), + ("", "", 0), + ("\n", "", 0), + ("Short description", "Short description", 0), + ("\nShort description\n", "Short description", 0), + ("\n Short description\n", "Short description", 0), ( "Short description\n\nLong description", "Short description\n\nLong description", + 0, ), ( """ @@ -970,6 +971,7 @@ def test_deprecation( Long description """, "Short description\n\nLong description", + 0, ), ( """ @@ -979,10 +981,12 @@ def test_deprecation( Second line """, "Short description\n\nLong description\nSecond line", + 0, ), ( "Short description\nLong description", "Short description\nLong description", + 0, ), ( """ @@ -990,10 +994,12 @@ def test_deprecation( Long description """, "Short description\nLong description", + 0, ), ( "\nShort description\nLong description\n", "Short description\nLong description", + 0, ), ( """ @@ -1002,6 +1008,7 @@ def test_deprecation( Second line """, "Short description\nLong description\nSecond line", + 0, ), ( """ @@ -1011,6 +1018,7 @@ def test_deprecation( asd """, "Short description\n\nMeta\n----\nasd", + 1, ), ( """ @@ -1025,6 +1033,7 @@ def test_deprecation( "Meta\n" "----\n" "asd", + 1, ), ( """ @@ -1041,6 +1050,7 @@ def test_deprecation( "Meta\n" "----\n" "asd", + 1, ), ( """ @@ -1059,6 +1069,7 @@ def test_deprecation( "Meta\n" "----\n" "asd", + 1, ), ( """ @@ -1077,6 +1088,7 @@ def test_deprecation( "Meta\n" "----\n" "asd", + 1, ), ( """ @@ -1096,6 +1108,7 @@ def test_deprecation( " 1\n" " 2\n" " 3", + 1, ), ( """ @@ -1114,7 +1127,7 @@ def test_deprecation( ----- derp """, - "Short description\n\n" + "Short description\n\n\n" "Meta1\n" "-----\n" "asd\n" @@ -1127,6 +1140,7 @@ def test_deprecation( "Meta3\n" "-----\n" "derp", + 3, ), ( """ @@ -1164,6 +1178,7 @@ def test_deprecation( " defaults to 'bye'\n" "default_arg : str, default=10\n" " description 6, defaults to 10", + 6, ), ( """ @@ -1178,6 +1193,7 @@ def test_deprecation( "------\n" "ValueError\n" " description", + 1, ), ( """ @@ -1204,17 +1220,18 @@ def test_deprecation( ">>> test2b\n" "desc2a\n" "desc2b", + 2, ), ], ) -def test_compose(source: str, expected: str) -> None: +def test_compose(source: str, expected: str, expected_num_metas: int) -> None: """Test compose in default mode.""" - # Test cases use `Meta#`, which are not included in the default sections. + # Test cases use `Meta` and `Meta#`, which are not included in the defaults. addtl_sections = [ - Section(f"Meta{i if i > 1 else ''}", f"meta{i if i > 1 else ''}") - for i in range(0, 4) - ] + Section(f"Meta{i}", f"meta{i}") + for i in range(1, 4) + ] + [Section("Meta", "meta")] parser_w_meta_section = NumpydocParser( sections=(DEFAULT_SECTIONS + addtl_sections) ) @@ -1222,9 +1239,6 @@ def test_compose(source: str, expected: str) -> None: # We want to make sure that parse is correctly parsing the docstring and # not returning the whole thing as a description. - if "-" in source: - assert len(docstring.meta) > 0 - else: - assert len(docstring.meta) == 0 + assert len(docstring.meta) == expected_num_metas assert compose(docstring) == expected From ddd5e9c9c854e980f6ddb85918b7e8ef3b9d44b1 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Tue, 17 Mar 2026 09:31:42 -0400 Subject: [PATCH 10/10] Linting fixes --- docstring_parser/tests/test_numpydoc.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docstring_parser/tests/test_numpydoc.py b/docstring_parser/tests/test_numpydoc.py index 29bf23d..0118a13 100644 --- a/docstring_parser/tests/test_numpydoc.py +++ b/docstring_parser/tests/test_numpydoc.py @@ -1227,11 +1227,10 @@ def test_deprecation( def test_compose(source: str, expected: str, expected_num_metas: int) -> None: """Test compose in default mode.""" - # Test cases use `Meta` and `Meta#`, which are not included in the defaults. - addtl_sections = [ - Section(f"Meta{i}", f"meta{i}") - for i in range(1, 4) - ] + [Section("Meta", "meta")] + # Test cases use `Meta` and `Meta#`, which aren't included in the defaults. + addtl_sections = [Section(f"Meta{i}", f"meta{i}") for i in range(1, 4)] + [ + Section("Meta", "meta") + ] parser_w_meta_section = NumpydocParser( sections=(DEFAULT_SECTIONS + addtl_sections) )