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.