From 3bea672938daca03d6abd4ea6c4f37077dd03e59 Mon Sep 17 00:00:00 2001 From: Dash Date: Fri, 6 Nov 2026 23:38:21 +0100 Subject: [PATCH] numpydoc: accept any underline length, and = A section title had to be underlined by exactly as many dashes as it has characters, so "Parameters" over "---" or over "==========" stayed in the long description. As in napoleon, an underline of two or more dashes or equals signs now does, whatever its length. Auto-detection looks for an underline of equals signs too, so it no longer takes such a docstring for one without sections. Headings no section declares still need an underline of dashes as long as the title, which tells them from tables. More text now reads as sections, which made compose show two ways a return did not read back: a named return with an empty type, written as its name alone and read back as its type, and an unnamed one with a colon in its type, read back as a name and a type. Compose writes them as "total :" and ": a: b". A param with no name is written with no space before its colon, which indented it. --- CHANGELOG.md | 1 + docstring_parser/auto.py | 7 ++- docstring_parser/numpydoc.py | 33 ++++++++-- ...py.lib._histograms_impl._get_bin_edges.txt | 60 ++++++++++++------- .../cases/numpydoc/compose_returns_01.txt | 38 ++++++++++++ .../tests/cases/numpydoc/underline_01.txt | 43 +++++++++++++ .../tests/cases/numpydoc/underline_02.txt | 45 ++++++++++++++ 7 files changed, 200 insertions(+), 27 deletions(-) create mode 100644 docstring_parser/tests/cases/numpydoc/compose_returns_01.txt create mode 100644 docstring_parser/tests/cases/numpydoc/underline_01.txt create mode 100644 docstring_parser/tests/cases/numpydoc/underline_02.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 12f7a86a..27e42a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ - Numpydoc: Keep indented text before a section's first item in the long description instead of dropping it - Numpydoc: Split a `Raises` or `Warns` item's head such as `ValueError : if bad.` after its type, taking `if bad.` as the start of the description, instead of making the whole line the type - Numpydoc: Compose sections and deprecations in the items' order, keeping the blank lines read +- Numpydoc: Accept a title's underline of any length, or of `=` signs - Epydoc: Match fields only at their header, so field-like text in a description no longer replaces it and a header without its colon no longer stalls the parser (thanks to @akshitkhokhani) - Epydoc: Take a param's key and kind from its `@param`, `@keyword` or `@ivar` field, instead of `type` when its `@type` field comes first - Epydoc: Give a name described by two fields, such as `@param x:` and `@ivar x:`, an item for each instead of keeping the last diff --git a/docstring_parser/auto.py b/docstring_parser/auto.py index c795e3c2..00a8e56e 100644 --- a/docstring_parser/auto.py +++ b/docstring_parser/auto.py @@ -9,9 +9,12 @@ # Any line that could start a field or a section in some style: a ReST # ``:field``, an Epydoc ``@field``, a Google title ending in a colon, or a -# Numpydoc dashes underline or ``..`` directive. +# Numpydoc underline of dashes or equals signs, or ``..`` directive. _FIELD_LINE = re.compile( - r"^[^\S\n]*[:@]" r"|:[^\S\n]*$" r"|^[^\S\n]*-+[^\S\n]*$" r"|^[^\S\n]*\.\.", + r"^[^\S\n]*[:@]" + r"|:[^\S\n]*$" + r"|^[^\S\n]*(?:-+|=+)[^\S\n]*$" + r"|^[^\S\n]*\.\.", flags=re.M, ) diff --git a/docstring_parser/numpydoc.py b/docstring_parser/numpydoc.py index b13fd4a8..defe6d0d 100644 --- a/docstring_parser/numpydoc.py +++ b/docstring_parser/numpydoc.py @@ -151,7 +151,7 @@ class Section: :param title: section title. For most sections, this is a heading like "Parameters" which appears on its own line, underlined by - en-dashes ('-') on the following line. + dashes ('-') or equals signs ('=') on the following line. :param key: meta key string. In the parsed ``DocstringMeta`` instance this is the ``key`` of the parsed ``DocstringMeta``. """ @@ -172,8 +172,9 @@ def _title_pattern(self) -> str: """ if self._spec is not None and self._spec.bare: return rf"^({re.escape(self.title)})[ \t]*$" - dashes = "-" * len(self.title) - return rf"^({re.escape(self.title)})\s*?\n{dashes}\s*$" + # As in napoleon, the underline may be shorter or longer than the + # title, and made of equals signs as a ReST heading's may be. + return rf"^({re.escape(self.title)})\s*?\n(?:-{{2,}}|={{2,}})\s*$" def _parse(self, text: str) -> T.Iterable[DocstringMeta]: """Parse ``DocstringMeta`` objects from the body of this section. @@ -660,11 +661,33 @@ def process_one( head = None if isinstance(one, DocstringParam) and written is not None: - head = f"{one.arg_name} : {written}" + # With no name, a space before the colon would indent the line. + head = ( + f"{one.arg_name} : {written}" + if one.arg_name + else f": {written}" + ) elif one.type_name and head: head += f" : {one.type_name}" elif one.type_name: - head = one.type_name + # A colon on the first line of a return's type would read back + # as one after its name, unless a colon before it says there + # is none. + if ( + isinstance(one, DocstringReturns) + and ":" in one.type_name.split("\n", 1)[0] + ): + head = f": {one.type_name}" + else: + head = one.type_name + elif ( + isinstance(one, DocstringReturns) + and head + and one.type_name is not None + ): + # A return's name alone would read back as its type; the colon + # after it says the type is empty. + head += " :" elif not head: head = "" diff --git a/docstring_parser/tests/cases/corpus/numpy/numpy.lib._histograms_impl._get_bin_edges.txt b/docstring_parser/tests/cases/corpus/numpy/numpy.lib._histograms_impl._get_bin_edges.txt index 6884cc58..ced3e835 100644 --- a/docstring_parser/tests/cases/corpus/numpy/numpy.lib._histograms_impl._get_bin_edges.txt +++ b/docstring_parser/tests/cases/corpus/numpy/numpy.lib._histograms_impl._get_bin_edges.txt @@ -22,33 +22,53 @@ === parse numpydoc short_description: 'Computes the bins used internally by `histogram`.' -long_description: | - Parameters - ========== - a : ndarray - Ravelled data array - bins, range - Forwarded arguments from `histogram`. - weights : ndarray, optional - Ravelled weights array, or None - - Returns - ======= - bin_edges : ndarray - Array of bin edges - uniform_bins : (Number, Number, int): - The upper bound, lowerbound, and number of bins, used in the optimized - implementation of `histogram` that works on uniform bins. blank_after_short_description: True blank_after_long_description: False style: numpydoc +- DocstringParam + description: 'Ravelled data array' + kind: param + key: 'param' + arg_name: 'a' + type_name: 'ndarray' + is_optional: False +- DocstringParam + description: 'Forwarded arguments from `histogram`.' + kind: param + key: 'param' + arg_name: 'bins, range' + is_optional: False +- DocstringParam + description: 'Ravelled weights array, or None' + kind: param + key: 'param' + arg_name: 'weights' + type_name: 'ndarray' + is_optional: True + raw_type_name: 'ndarray, optional' +- DocstringReturns + description: 'Array of bin edges' + kind: returns + key: 'returns' + type_name: 'ndarray' + is_generator: False + return_name: 'bin_edges' +- DocstringReturns + description: | + The upper bound, lowerbound, and number of bins, used in the optimized + implementation of `histogram` that works on uniform bins. + kind: returns + key: 'returns' + type_name: '(Number, Number, int):' + is_generator: False + return_name: 'uniform_bins' === detect -rest +numpydoc === compose numpydoc Computes the bins used internally by `histogram`. Parameters -========== +---------- a : ndarray Ravelled data array bins, range @@ -57,7 +77,7 @@ weights : ndarray, optional Ravelled weights array, or None Returns -======= +------- bin_edges : ndarray Array of bin edges uniform_bins : (Number, Number, int): diff --git a/docstring_parser/tests/cases/numpydoc/compose_returns_01.txt b/docstring_parser/tests/cases/numpydoc/compose_returns_01.txt new file mode 100644 index 00000000..31ba7bc4 --- /dev/null +++ b/docstring_parser/tests/cases/numpydoc/compose_returns_01.txt @@ -0,0 +1,38 @@ +# Test that compose keeps a return's empty type, and a colon in its type. +=== input +Short. + +Returns +------- +total : + The total. +: a: b + Odd. +=== parse numpydoc +short_description: 'Short.' +blank_after_short_description: True +blank_after_long_description: False +style: numpydoc +- DocstringReturns + description: 'The total.' + kind: returns + key: 'returns' + type_name: '' + is_generator: False + return_name: 'total' +- DocstringReturns + description: 'Odd.' + kind: returns + key: 'returns' + type_name: 'a: b' + is_generator: False + return_name: '' +=== compose numpydoc +Short. + +Returns +------- +total : + The total. +: a: b + Odd. diff --git a/docstring_parser/tests/cases/numpydoc/underline_01.txt b/docstring_parser/tests/cases/numpydoc/underline_01.txt new file mode 100644 index 00000000..00caca97 --- /dev/null +++ b/docstring_parser/tests/cases/numpydoc/underline_01.txt @@ -0,0 +1,43 @@ +# Test that a title's underline may be shorter or longer than the title. +=== input +Short. + +Parameters +--- +x : int + The x. + +Returns +-------------- +int + The sum. +=== parse numpydoc +short_description: 'Short.' +blank_after_short_description: True +blank_after_long_description: False +style: numpydoc +- DocstringParam + description: 'The x.' + kind: param + key: 'param' + arg_name: 'x' + type_name: 'int' + is_optional: False +- DocstringReturns + description: 'The sum.' + kind: returns + key: 'returns' + type_name: 'int' + is_generator: False +=== compose numpydoc +Short. + +Parameters +---------- +x : int + The x. + +Returns +------- +int + The sum. diff --git a/docstring_parser/tests/cases/numpydoc/underline_02.txt b/docstring_parser/tests/cases/numpydoc/underline_02.txt new file mode 100644 index 00000000..d9e2b784 --- /dev/null +++ b/docstring_parser/tests/cases/numpydoc/underline_02.txt @@ -0,0 +1,45 @@ +# Test that a title may be underlined with equals signs. +=== input +Short. + +Parameters +========== +x : int + The x. + +Returns +======= +int + The sum. +=== parse numpydoc +short_description: 'Short.' +blank_after_short_description: True +blank_after_long_description: False +style: numpydoc +- DocstringParam + description: 'The x.' + kind: param + key: 'param' + arg_name: 'x' + type_name: 'int' + is_optional: False +- DocstringReturns + description: 'The sum.' + kind: returns + key: 'returns' + type_name: 'int' + is_generator: False +=== detect +numpydoc +=== compose numpydoc +Short. + +Parameters +---------- +x : int + The x. + +Returns +------- +int + The sum.