"""Tests for :mod:`sphinx.ext.napoleon.docstring` module.""" import re import zlib from collections import namedtuple from inspect import cleandoc from itertools import product from textwrap import dedent from unittest import mock import pytest from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping from sphinx.ext.napoleon import Config from sphinx.ext.napoleon.docstring import ( GoogleDocstring, NumpyDocstring, _convert_numpy_type_spec, _recombine_set_tokens, _token_type, _tokenize_type_spec, ) from sphinx.testing.util import etree_parse from tests.test_extensions.ext_napoleon_pep526_data_google import PEP526GoogleClass from tests.test_extensions.ext_napoleon_pep526_data_numpy import PEP526NumpyClass class NamedtupleSubclass(namedtuple('NamedtupleSubclass', ('attr1', 'attr2'))): """Sample namedtuple subclass Attributes ---------- attr1 : Arbitrary type Quick description of attr1 attr2 : Another arbitrary type Quick description of attr2 attr3 : Type Adds a newline after the type """ # To avoid creating a dict, as a namedtuple doesn't have it: __slots__ = () def __new__(cls, attr1, attr2=None): return super().__new__(cls, attr1, attr2) class TestNamedtupleSubclass: def test_attributes_docstring(self): config = Config() actual = str(NumpyDocstring(cleandoc(NamedtupleSubclass.__doc__), config=config, app=None, what='class', name='NamedtupleSubclass', obj=NamedtupleSubclass)) expected = """\ Sample namedtuple subclass .. attribute:: attr1 Quick description of attr1 :type: Arbitrary type .. attribute:: attr2 Quick description of attr2 :type: Another arbitrary type .. attribute:: attr3 Adds a newline after the type :type: Type """ assert expected == actual class TestInlineAttribute: inline_google_docstring = ('inline description with ' '``a : in code``, ' 'a :ref:`reference`, ' 'a `link `_, ' 'a :meta public:, ' 'a :meta field: value and ' 'an host:port and HH:MM strings.') @staticmethod def _docstring(source): rst = GoogleDocstring(source, config=Config(), app=None, what='attribute', name='some_data', obj=0) return str(rst) def test_class_data_member(self): source = 'data member description:\n\n- a: b' actual = self._docstring(source).splitlines() assert actual == ['data member description:', '', '- a: b'] def test_class_data_member_inline(self): source = f'CustomType: {self.inline_google_docstring}' actual = self._docstring(source).splitlines() assert actual == [self.inline_google_docstring, '', ':type: CustomType'] def test_class_data_member_inline_no_type(self): source = self.inline_google_docstring actual = self._docstring(source).splitlines() assert actual == [source] def test_class_data_member_inline_ref_in_type(self): source = f':class:`int`: {self.inline_google_docstring}' actual = self._docstring(source).splitlines() assert actual == [self.inline_google_docstring, '', ':type: :class:`int`'] class TestGoogleDocstring: docstrings = [( """Single line summary""", """Single line summary""", ), ( """ Single line summary Extended description """, """ Single line summary Extended description """, ), ( """ Single line summary Args: arg1(str):Extended description of arg1 """, """ Single line summary :Parameters: **arg1** (*str*) -- Extended description of arg1 """, ), ( """ Single line summary Args: arg1(str):Extended description of arg1 arg2 ( int ) : Extended description of arg2 Keyword Args: kwarg1(str):Extended description of kwarg1 kwarg2 ( int ) : Extended description of kwarg2""", """ Single line summary :Parameters: * **arg1** (*str*) -- Extended description of arg1 * **arg2** (*int*) -- Extended description of arg2 :Keyword Arguments: * **kwarg1** (*str*) -- Extended description of kwarg1 * **kwarg2** (*int*) -- Extended description of kwarg2 """, ), ( """ Single line summary Arguments: arg1(str):Extended description of arg1 arg2 ( int ) : Extended description of arg2 Keyword Arguments: kwarg1(str):Extended description of kwarg1 kwarg2 ( int ) : Extended description of kwarg2""", """ Single line summary :Parameters: * **arg1** (*str*) -- Extended description of arg1 * **arg2** (*int*) -- Extended description of arg2 :Keyword Arguments: * **kwarg1** (*str*) -- Extended description of kwarg1 * **kwarg2** (*int*) -- Extended description of kwarg2 """, ), ( """ Single line summary Return: str:Extended description of return value """, """ Single line summary :returns: *str* -- Extended description of return value """, ), ( """ Single line summary Returns: str:Extended description of return value """, """ Single line summary :returns: *str* -- Extended description of return value """, ), ( """ Single line summary Returns: Extended description of return value """, """ Single line summary :returns: Extended description of return value """, ), ( """ Single line summary Returns: Extended """, """ Single line summary :returns: Extended """, ), ( """ Single line summary Args: arg1(str):Extended description of arg1 *args: Variable length argument list. **kwargs: Arbitrary keyword arguments. """, """ Single line summary :Parameters: * **arg1** (*str*) -- Extended description of arg1 * **\\*args** -- Variable length argument list. * **\\*\\*kwargs** -- Arbitrary keyword arguments. """, ), ( """ Single line summary Args: arg1 (list(int)): Description arg2 (list[int]): Description arg3 (dict(str, int)): Description arg4 (dict[str, int]): Description """, """ Single line summary :Parameters: * **arg1** (*list(int)*) -- Description * **arg2** (*list[int]*) -- Description * **arg3** (*dict(str, int)*) -- Description * **arg4** (*dict[str, int]*) -- Description """, ), ( """ Single line summary Receive: arg1 (list(int)): Description arg2 (list[int]): Description """, """ Single line summary :Receives: * **arg1** (*list(int)*) -- Description * **arg2** (*list[int]*) -- Description """, ), ( """ Single line summary Receives: arg1 (list(int)): Description arg2 (list[int]): Description """, """ Single line summary :Receives: * **arg1** (*list(int)*) -- Description * **arg2** (*list[int]*) -- Description """, ), ( """ Single line summary Yield: str:Extended description of yielded value """, """ Single line summary :Yields: *str* -- Extended description of yielded value """, ), ( """ Single line summary Yields: Extended description of yielded value """, """ Single line summary :Yields: Extended description of yielded value """, ), ( """ Single line summary Args: arg1 (list of str): Extended description of arg1. arg2 (tuple of int): Extended description of arg2. arg3 (tuple of list of float): Extended description of arg3. arg4 (int, float, or list of bool): Extended description of arg4. arg5 (list of int, float, or bool): Extended description of arg5. arg6 (list of int or float): Extended description of arg6. """, """ Single line summary :Parameters: * **arg1** (*list of str*) -- Extended description of arg1. * **arg2** (*tuple of int*) -- Extended description of arg2. * **arg3** (*tuple of list of float*) -- Extended description of arg3. * **arg4** (*int, float, or list of bool*) -- Extended description of arg4. * **arg5** (*list of int, float, or bool*) -- Extended description of arg5. * **arg6** (*list of int or float*) -- Extended description of arg6. """, )] def test_sphinx_admonitions(self): admonition_map = { 'Attention': 'attention', 'Caution': 'caution', 'Danger': 'danger', 'Error': 'error', 'Hint': 'hint', 'Important': 'important', 'Note': 'note', 'Tip': 'tip', 'Todo': 'todo', 'Warning': 'warning', 'Warnings': 'warning', } config = Config() for section, admonition in admonition_map.items(): # Multiline actual = str(GoogleDocstring(f"{section}:\n" " this is the first line\n" "\n" " and this is the second line\n", config)) expect = (f".. {admonition}::\n" "\n" " this is the first line\n" " \n" " and this is the second line\n" ) assert expect == actual # Single line actual = str(GoogleDocstring(f"{section}:\n" " this is a single line\n", config)) expect = f".. {admonition}:: this is a single line\n" assert expect == actual def test_docstrings(self): config = Config( napoleon_use_param=False, napoleon_use_rtype=False, napoleon_use_keyword=False, ) for docstring, expected in self.docstrings: actual = str(GoogleDocstring(dedent(docstring), config)) expected = dedent(expected) assert expected == actual def test_parameters_with_class_reference(self): docstring = """\ Construct a new XBlock. This class should only be used by runtimes. Arguments: runtime (:class:`~typing.Dict`\\[:class:`int`,:class:`str`\\]): Use it to access the environment. It is available in XBlock code as ``self.runtime``. field_data (:class:`FieldData`): Interface used by the XBlock fields to access their data from wherever it is persisted. scope_ids (:class:`ScopeIds`): Identifiers needed to resolve scopes. """ actual = str(GoogleDocstring(docstring)) expected = """\ Construct a new XBlock. This class should only be used by runtimes. :param runtime: Use it to access the environment. It is available in XBlock code as ``self.runtime``. :type runtime: :class:`~typing.Dict`\\[:class:`int`,:class:`str`\\] :param field_data: Interface used by the XBlock fields to access their data from wherever it is persisted. :type field_data: :class:`FieldData` :param scope_ids: Identifiers needed to resolve scopes. :type scope_ids: :class:`ScopeIds` """ assert expected == actual def test_attributes_with_class_reference(self): docstring = """\ Attributes: in_attr(:class:`numpy.ndarray`): super-dooper attribute """ actual = str(GoogleDocstring(docstring)) expected = """\ .. attribute:: in_attr super-dooper attribute :type: :class:`numpy.ndarray` """ assert expected == actual docstring = """\ Attributes: in_attr(numpy.ndarray): super-dooper attribute """ actual = str(GoogleDocstring(docstring)) expected = """\ .. attribute:: in_attr super-dooper attribute :type: numpy.ndarray """ def test_attributes_with_use_ivar(self): docstring = """\ Attributes: foo (int): blah blah bar (str): blah blah """ config = Config(napoleon_use_ivar=True) actual = str(GoogleDocstring(docstring, config, obj=self.__class__)) expected = """\ :ivar foo: blah blah :vartype foo: int :ivar bar: blah blah :vartype bar: str """ assert expected == actual def test_code_block_in_returns_section(self): docstring = """ Returns: foobar: foo:: codecode codecode """ expected = """ :returns: foo:: codecode codecode :rtype: foobar """ actual = str(GoogleDocstring(docstring)) assert expected == actual def test_colon_in_return_type(self): docstring = """Example property. Returns: :py:class:`~.module.submodule.SomeClass`: an example instance if available, None if not available. """ expected = """Example property. :returns: an example instance if available, None if not available. :rtype: :py:class:`~.module.submodule.SomeClass` """ actual = str(GoogleDocstring(docstring)) assert expected == actual def test_xrefs_in_return_type(self): docstring = """Example Function Returns: :class:`numpy.ndarray`: A :math:`n \\times 2` array containing a bunch of math items """ expected = """Example Function :returns: A :math:`n \\times 2` array containing a bunch of math items :rtype: :class:`numpy.ndarray` """ actual = str(GoogleDocstring(docstring)) assert expected == actual def test_raises_types(self): docstrings = [(""" Example Function Raises: RuntimeError: A setting wasn't specified, or was invalid. ValueError: Something something value error. :py:class:`AttributeError` errors for missing attributes. ~InvalidDimensionsError If the dimensions couldn't be parsed. `InvalidArgumentsError` If the arguments are invalid. :exc:`~ValueError` If the arguments are wrong. """, """ Example Function :raises RuntimeError: A setting wasn't specified, or was invalid. :raises ValueError: Something something value error. :raises AttributeError: errors for missing attributes. :raises ~InvalidDimensionsError: If the dimensions couldn't be parsed. :raises InvalidArgumentsError: If the arguments are invalid. :raises ~ValueError: If the arguments are wrong. """), ################################ (""" Example Function Raises: InvalidDimensionsError """, """ Example Function :raises InvalidDimensionsError: """), ################################ (""" Example Function Raises: Invalid Dimensions Error """, """ Example Function :raises Invalid Dimensions Error: """), ################################ (""" Example Function Raises: Invalid Dimensions Error: With description """, """ Example Function :raises Invalid Dimensions Error: With description """), ################################ (""" Example Function Raises: InvalidDimensionsError: If the dimensions couldn't be parsed. """, """ Example Function :raises InvalidDimensionsError: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises: Invalid Dimensions Error: If the dimensions couldn't be parsed. """, """ Example Function :raises Invalid Dimensions Error: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises: If the dimensions couldn't be parsed. """, """ Example Function :raises If the dimensions couldn't be parsed.: """), ################################ (""" Example Function Raises: :class:`exc.InvalidDimensionsError` """, """ Example Function :raises exc.InvalidDimensionsError: """), ################################ (""" Example Function Raises: :class:`exc.InvalidDimensionsError`: If the dimensions couldn't be parsed. """, """ Example Function :raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises: :class:`exc.InvalidDimensionsError`: If the dimensions couldn't be parsed, then a :class:`exc.InvalidDimensionsError` will be raised. """, """ Example Function :raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed, then a :class:`exc.InvalidDimensionsError` will be raised. """), ################################ (""" Example Function Raises: :class:`exc.InvalidDimensionsError`: If the dimensions couldn't be parsed. :class:`exc.InvalidArgumentsError`: If the arguments are invalid. """, """ Example Function :raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed. :raises exc.InvalidArgumentsError: If the arguments are invalid. """), ################################ (""" Example Function Raises: :class:`exc.InvalidDimensionsError` :class:`exc.InvalidArgumentsError` """, """ Example Function :raises exc.InvalidDimensionsError: :raises exc.InvalidArgumentsError: """)] for docstring, expected in docstrings: actual = str(GoogleDocstring(docstring)) assert expected == actual def test_kwargs_in_arguments(self): docstring = """Allows to create attributes binded to this device. Some other paragraph. Code sample for usage:: dev.bind(loopback=Loopback) dev.loopback.configure() Arguments: **kwargs: name/class pairs that will create resource-managers bound as instance attributes to this instance. See code example above. """ expected = """Allows to create attributes binded to this device. Some other paragraph. Code sample for usage:: dev.bind(loopback=Loopback) dev.loopback.configure() :param \\*\\*kwargs: name/class pairs that will create resource-managers bound as instance attributes to this instance. See code example above. """ actual = str(GoogleDocstring(docstring)) assert expected == actual def test_section_header_formatting(self): docstrings = [(""" Summary line Example: Multiline reStructuredText literal code block """, """ Summary line .. rubric:: Example Multiline reStructuredText literal code block """), ################################ (""" Summary line Example:: Multiline reStructuredText literal code block """, """ Summary line Example:: Multiline reStructuredText literal code block """), ################################ (""" Summary line :Example: Multiline reStructuredText literal code block """, """ Summary line :Example: Multiline reStructuredText literal code block """)] for docstring, expected in docstrings: actual = str(GoogleDocstring(docstring)) assert expected == actual def test_list_in_parameter_description(self): docstring = """One line summary. Parameters: no_list (int): one_bullet_empty (int): * one_bullet_single_line (int): - first line one_bullet_two_lines (int): + first line continued two_bullets_single_line (int): - first line - second line two_bullets_two_lines (int): * first line continued * second line continued one_enumeration_single_line (int): 1. first line one_enumeration_two_lines (int): 1) first line continued two_enumerations_one_line (int): (iii) first line (iv) second line two_enumerations_two_lines (int): a. first line continued b. second line continued one_definition_one_line (int): item 1 first line one_definition_two_lines (int): item 1 first line continued two_definitions_one_line (int): item 1 first line item 2 second line two_definitions_two_lines (int): item 1 first line continued item 2 second line continued one_definition_blank_line (int): item 1 first line extra first line two_definitions_blank_lines (int): item 1 first line extra first line item 2 second line extra second line definition_after_inline_text (int): text line item 1 first line definition_after_normal_text (int): text line item 1 first line """ expected = """One line summary. :param no_list: :type no_list: int :param one_bullet_empty: * :type one_bullet_empty: int :param one_bullet_single_line: - first line :type one_bullet_single_line: int :param one_bullet_two_lines: + first line continued :type one_bullet_two_lines: int :param two_bullets_single_line: - first line - second line :type two_bullets_single_line: int :param two_bullets_two_lines: * first line continued * second line continued :type two_bullets_two_lines: int :param one_enumeration_single_line: 1. first line :type one_enumeration_single_line: int :param one_enumeration_two_lines: 1) first line continued :type one_enumeration_two_lines: int :param two_enumerations_one_line: (iii) first line (iv) second line :type two_enumerations_one_line: int :param two_enumerations_two_lines: a. first line continued b. second line continued :type two_enumerations_two_lines: int :param one_definition_one_line: item 1 first line :type one_definition_one_line: int :param one_definition_two_lines: item 1 first line continued :type one_definition_two_lines: int :param two_definitions_one_line: item 1 first line item 2 second line :type two_definitions_one_line: int :param two_definitions_two_lines: item 1 first line continued item 2 second line continued :type two_definitions_two_lines: int :param one_definition_blank_line: item 1 first line extra first line :type one_definition_blank_line: int :param two_definitions_blank_lines: item 1 first line extra first line item 2 second line extra second line :type two_definitions_blank_lines: int :param definition_after_inline_text: text line item 1 first line :type definition_after_inline_text: int :param definition_after_normal_text: text line item 1 first line :type definition_after_normal_text: int """ config = Config(napoleon_use_param=True) actual = str(GoogleDocstring(docstring, config)) assert expected == actual expected = """One line summary. :Parameters: * **no_list** (*int*) * **one_bullet_empty** (*int*) -- * * **one_bullet_single_line** (*int*) -- - first line * **one_bullet_two_lines** (*int*) -- + first line continued * **two_bullets_single_line** (*int*) -- - first line - second line * **two_bullets_two_lines** (*int*) -- * first line continued * second line continued * **one_enumeration_single_line** (*int*) -- 1. first line * **one_enumeration_two_lines** (*int*) -- 1) first line continued * **two_enumerations_one_line** (*int*) -- (iii) first line (iv) second line * **two_enumerations_two_lines** (*int*) -- a. first line continued b. second line continued * **one_definition_one_line** (*int*) -- item 1 first line * **one_definition_two_lines** (*int*) -- item 1 first line continued * **two_definitions_one_line** (*int*) -- item 1 first line item 2 second line * **two_definitions_two_lines** (*int*) -- item 1 first line continued item 2 second line continued * **one_definition_blank_line** (*int*) -- item 1 first line extra first line * **two_definitions_blank_lines** (*int*) -- item 1 first line extra first line item 2 second line extra second line * **definition_after_inline_text** (*int*) -- text line item 1 first line * **definition_after_normal_text** (*int*) -- text line item 1 first line """ config = Config(napoleon_use_param=False) actual = str(GoogleDocstring(docstring, config)) assert expected == actual def test_custom_generic_sections(self): docstrings = (("""\ Really Important Details: You should listen to me! """, """.. rubric:: Really Important Details You should listen to me! """), ("""\ Sooper Warning: Stop hitting yourself! """, """:Warns: **Stop hitting yourself!** """), ("""\ Params Style: arg1 (int): Description of arg1 arg2 (str): Description of arg2 """, """\ :Params Style: * **arg1** (*int*) -- Description of arg1 * **arg2** (*str*) -- Description of arg2 """), ("""\ Returns Style: description of custom section """, """:Returns Style: description of custom section """)) testConfig = Config(napoleon_custom_sections=['Really Important Details', ('Sooper Warning', 'warns'), ('Params Style', 'params_style'), ('Returns Style', 'returns_style')]) for docstring, expected in docstrings: actual = str(GoogleDocstring(docstring, testConfig)) assert expected == actual def test_noindex(self): docstring = """ Attributes: arg description Methods: func(i, j) description """ expected = """ .. attribute:: arg :no-index: description .. method:: func(i, j) :no-index: description """ # NoQA: W293 config = Config() actual = str(GoogleDocstring(docstring, config=config, app=None, what='module', options={'no-index': True})) assert expected == actual def test_keywords_with_types(self): docstring = """\ Do as you please Keyword Args: gotham_is_yours (None): shall interfere. """ actual = str(GoogleDocstring(docstring)) expected = """\ Do as you please :keyword gotham_is_yours: shall interfere. :kwtype gotham_is_yours: None """ assert expected == actual def test_pep526_annotations(self): # Test class attributes annotations config = Config( napoleon_attr_annotations=True, ) actual = str(GoogleDocstring(cleandoc(PEP526GoogleClass.__doc__), config, app=None, what="class", obj=PEP526GoogleClass)) expected = """\ Sample class with PEP 526 annotations and google docstring. .. attribute:: attr1 Attr1 description. :type: int .. attribute:: attr2 Attr2 description. :type: str """ assert expected == actual def test_preprocess_types(self): docstring = """\ Do as you please Yield: str:Extended """ actual = str(GoogleDocstring(docstring)) expected = """\ Do as you please :Yields: *str* -- Extended """ assert expected == actual config = Config(napoleon_preprocess_types=True) actual = str(GoogleDocstring(docstring, config)) expected = """\ Do as you please :Yields: :py:class:`str` -- Extended """ assert expected == actual class TestNumpyDocstring: docstrings = [( """Single line summary""", """Single line summary""", ), ( """ Single line summary Extended description """, """ Single line summary Extended description """, ), ( """ Single line summary Parameters ---------- arg1:str Extended description of arg1 """, """ Single line summary :Parameters: **arg1** (:class:`str`) -- Extended description of arg1 """, ), ( """ Single line summary Parameters ---------- arg1:str Extended description of arg1 arg2 : int Extended description of arg2 Keyword Arguments ----------------- kwarg1:str Extended description of kwarg1 kwarg2 : int Extended description of kwarg2 """, """ Single line summary :Parameters: * **arg1** (:class:`str`) -- Extended description of arg1 * **arg2** (:class:`int`) -- Extended description of arg2 :Keyword Arguments: * **kwarg1** (:class:`str`) -- Extended description of kwarg1 * **kwarg2** (:class:`int`) -- Extended description of kwarg2 """, ), ( """ Single line summary Return ------ str Extended description of return value """, """ Single line summary :returns: :class:`str` -- Extended description of return value """, ), ( """ Single line summary Returns ------- str Extended description of return value """, """ Single line summary :returns: :class:`str` -- Extended description of return value """, ), ( """ Single line summary Parameters ---------- arg1:str Extended description of arg1 *args: Variable length argument list. **kwargs: Arbitrary keyword arguments. """, """ Single line summary :Parameters: * **arg1** (:class:`str`) -- Extended description of arg1 * **\\*args** -- Variable length argument list. * **\\*\\*kwargs** -- Arbitrary keyword arguments. """, ), ( """ Single line summary Parameters ---------- arg1:str Extended description of arg1 *args, **kwargs: Variable length argument list and arbitrary keyword arguments. """, """ Single line summary :Parameters: * **arg1** (:class:`str`) -- Extended description of arg1 * **\\*args, \\*\\*kwargs** -- Variable length argument list and arbitrary keyword arguments. """, ), ( """ Single line summary Receive ------- arg1:str Extended description of arg1 arg2 : int Extended description of arg2 """, """ Single line summary :Receives: * **arg1** (:class:`str`) -- Extended description of arg1 * **arg2** (:class:`int`) -- Extended description of arg2 """, ), ( """ Single line summary Receives -------- arg1:str Extended description of arg1 arg2 : int Extended description of arg2 """, """ Single line summary :Receives: * **arg1** (:class:`str`) -- Extended description of arg1 * **arg2** (:class:`int`) -- Extended description of arg2 """, ), ( """ Single line summary Yield ----- str Extended description of yielded value """, """ Single line summary :Yields: :class:`str` -- Extended description of yielded value """, ), ( """ Single line summary Yields ------ str Extended description of yielded value """, """ Single line summary :Yields: :class:`str` -- Extended description of yielded value """, )] def test_sphinx_admonitions(self): admonition_map = { 'Attention': 'attention', 'Caution': 'caution', 'Danger': 'danger', 'Error': 'error', 'Hint': 'hint', 'Important': 'important', 'Note': 'note', 'Tip': 'tip', 'Todo': 'todo', 'Warning': 'warning', 'Warnings': 'warning', } config = Config() for section, admonition in admonition_map.items(): # Multiline actual = str(NumpyDocstring(f"{section}\n" f"{'-' * len(section)}\n" " this is the first line\n" "\n" " and this is the second line\n", config)) expect = (f".. {admonition}::\n" "\n" " this is the first line\n" " \n" " and this is the second line\n" ) assert expect == actual # Single line actual = str(NumpyDocstring(f"{section}\n" f"{'-' * len(section)}\n" f" this is a single line\n", config)) expect = f".. {admonition}:: this is a single line\n" assert expect == actual def test_docstrings(self): config = Config( napoleon_use_param=False, napoleon_use_rtype=False, napoleon_use_keyword=False, napoleon_preprocess_types=True) for docstring, expected in self.docstrings: actual = str(NumpyDocstring(dedent(docstring), config)) expected = dedent(expected) assert expected == actual def test_type_preprocessor(self): docstring = dedent(""" Single line summary Parameters ---------- arg1:str Extended description of arg1 """) config = Config(napoleon_preprocess_types=False, napoleon_use_param=False) actual = str(NumpyDocstring(docstring, config)) expected = dedent(""" Single line summary :Parameters: **arg1** (*str*) -- Extended description of arg1 """) assert expected == actual def test_parameters_with_class_reference(self): docstring = """\ Parameters ---------- param1 : :class:`MyClass ` instance Other Parameters ---------------- param2 : :class:`MyClass ` instance """ config = Config(napoleon_use_param=False) actual = str(NumpyDocstring(docstring, config)) expected = """\ :Parameters: **param1** (:class:`MyClass ` instance) :Other Parameters: **param2** (:class:`MyClass ` instance) """ assert expected == actual config = Config(napoleon_use_param=True) actual = str(NumpyDocstring(docstring, config)) expected = """\ :param param1: :type param1: :class:`MyClass ` instance :param param2: :type param2: :class:`MyClass ` instance """ assert expected == actual def test_multiple_parameters(self): docstring = """\ Parameters ---------- x1, x2 : array_like Input arrays, description of ``x1``, ``x2``. """ config = Config(napoleon_use_param=False) actual = str(NumpyDocstring(docstring, config)) expected = """\ :Parameters: **x1, x2** (*array_like*) -- Input arrays, description of ``x1``, ``x2``. """ assert expected == actual config = Config(napoleon_use_param=True) actual = str(NumpyDocstring(dedent(docstring), config)) expected = """\ :param x1: Input arrays, description of ``x1``, ``x2``. :type x1: array_like :param x2: Input arrays, description of ``x1``, ``x2``. :type x2: array_like """ assert expected == actual def test_parameters_without_class_reference(self): docstring = """\ Parameters ---------- param1 : MyClass instance """ config = Config(napoleon_use_param=False) actual = str(NumpyDocstring(docstring, config)) expected = """\ :Parameters: **param1** (*MyClass instance*) """ assert expected == actual config = Config(napoleon_use_param=True) actual = str(NumpyDocstring(dedent(docstring), config)) expected = """\ :param param1: :type param1: MyClass instance """ assert expected == actual def test_see_also_refs(self): docstring = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) See Also -------- some, other, funcs otherfunc : relationship """ actual = str(NumpyDocstring(docstring)) expected = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) .. seealso:: :obj:`some`, :obj:`other`, :obj:`funcs` \n\ :obj:`otherfunc` relationship """ assert expected == actual docstring = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) See Also -------- some, other, funcs otherfunc : relationship """ config = Config() app = mock.Mock() actual = str(NumpyDocstring(docstring, config, app, "method")) expected = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) .. seealso:: :obj:`some`, :obj:`other`, :obj:`funcs` \n\ :obj:`otherfunc` relationship """ assert expected == actual docstring = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) See Also -------- some, other, :func:`funcs` otherfunc : relationship """ translations = { "other": "MyClass.other", "otherfunc": ":func:`~my_package.otherfunc`", } config = Config(napoleon_type_aliases=translations) app = mock.Mock() actual = str(NumpyDocstring(docstring, config, app, "method")) expected = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) .. seealso:: :obj:`some`, :obj:`MyClass.other`, :func:`funcs` \n\ :func:`~my_package.otherfunc` relationship """ assert expected == actual def test_colon_in_return_type(self): docstring = """ Summary Returns ------- :py:class:`~my_mod.my_class` an instance of :py:class:`~my_mod.my_class` """ expected = """ Summary :returns: an instance of :py:class:`~my_mod.my_class` :rtype: :py:class:`~my_mod.my_class` """ config = Config() app = mock.Mock() actual = str(NumpyDocstring(docstring, config, app, "method")) assert expected == actual def test_underscore_in_attribute(self): docstring = """ Attributes ---------- arg_ : type some description """ expected = """ :ivar arg_: some description :vartype arg_: type """ config = Config(napoleon_use_ivar=True) app = mock.Mock() actual = str(NumpyDocstring(docstring, config, app, "class")) assert expected == actual def test_underscore_in_attribute_strip_signature_backslash(self): docstring = """ Attributes ---------- arg_ : type some description """ expected = """ :ivar arg\\_: some description :vartype arg\\_: type """ config = Config(napoleon_use_ivar=True) config.strip_signature_backslash = True app = mock.Mock() actual = str(NumpyDocstring(docstring, config, app, "class")) assert expected == actual def test_return_types(self): docstring = dedent(""" Returns ------- DataFrame a dataframe """) expected = dedent(""" :returns: a dataframe :rtype: :class:`~pandas.DataFrame` """) translations = { "DataFrame": "~pandas.DataFrame", } config = Config( napoleon_use_param=True, napoleon_use_rtype=True, napoleon_preprocess_types=True, napoleon_type_aliases=translations, ) actual = str(NumpyDocstring(docstring, config)) assert expected == actual def test_yield_types(self): docstring = dedent(""" Example Function Yields ------ scalar or array-like The result of the computation """) expected = dedent(""" Example Function :Yields: :term:`scalar` or :class:`array-like ` -- The result of the computation """) translations = { "scalar": ":term:`scalar`", "array-like": ":class:`array-like `", } config = Config(napoleon_type_aliases=translations, napoleon_preprocess_types=True) app = mock.Mock() actual = str(NumpyDocstring(docstring, config, app, "method")) assert expected == actual def test_raises_types(self): docstrings = [(""" Example Function Raises ------ RuntimeError A setting wasn't specified, or was invalid. ValueError Something something value error. """, """ Example Function :raises RuntimeError: A setting wasn't specified, or was invalid. :raises ValueError: Something something value error. """), ################################ (""" Example Function Raises ------ InvalidDimensionsError """, """ Example Function :raises InvalidDimensionsError: """), ################################ (""" Example Function Raises ------ Invalid Dimensions Error """, """ Example Function :raises Invalid Dimensions Error: """), ################################ (""" Example Function Raises ------ Invalid Dimensions Error With description """, """ Example Function :raises Invalid Dimensions Error: With description """), ################################ (""" Example Function Raises ------ InvalidDimensionsError If the dimensions couldn't be parsed. """, """ Example Function :raises InvalidDimensionsError: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises ------ Invalid Dimensions Error If the dimensions couldn't be parsed. """, """ Example Function :raises Invalid Dimensions Error: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises ------ If the dimensions couldn't be parsed. """, """ Example Function :raises If the dimensions couldn't be parsed.: """), ################################ (""" Example Function Raises ------ :class:`exc.InvalidDimensionsError` """, """ Example Function :raises exc.InvalidDimensionsError: """), ################################ (""" Example Function Raises ------ :class:`exc.InvalidDimensionsError` If the dimensions couldn't be parsed. """, """ Example Function :raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises ------ :class:`exc.InvalidDimensionsError` If the dimensions couldn't be parsed, then a :class:`exc.InvalidDimensionsError` will be raised. """, """ Example Function :raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed, then a :class:`exc.InvalidDimensionsError` will be raised. """), ################################ (""" Example Function Raises ------ :class:`exc.InvalidDimensionsError` If the dimensions couldn't be parsed. :class:`exc.InvalidArgumentsError` If the arguments are invalid. """, """ Example Function :raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed. :raises exc.InvalidArgumentsError: If the arguments are invalid. """), ################################ (""" Example Function Raises ------ CustomError If the dimensions couldn't be parsed. """, """ Example Function :raises package.CustomError: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises ------ AnotherError If the dimensions couldn't be parsed. """, """ Example Function :raises ~package.AnotherError: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises ------ :class:`exc.InvalidDimensionsError` :class:`exc.InvalidArgumentsError` """, """ Example Function :raises exc.InvalidDimensionsError: :raises exc.InvalidArgumentsError: """)] for docstring, expected in docstrings: translations = { "CustomError": "package.CustomError", "AnotherError": ":py:exc:`~package.AnotherError`", } config = Config(napoleon_type_aliases=translations, napoleon_preprocess_types=True) app = mock.Mock() actual = str(NumpyDocstring(docstring, config, app, "method")) assert expected == actual def test_xrefs_in_return_type(self): docstring = """ Example Function Returns ------- :class:`numpy.ndarray` A :math:`n \\times 2` array containing a bunch of math items """ expected = """ Example Function :returns: A :math:`n \\times 2` array containing a bunch of math items :rtype: :class:`numpy.ndarray` """ config = Config() app = mock.Mock() actual = str(NumpyDocstring(docstring, config, app, "method")) assert expected == actual def test_section_header_underline_length(self): docstrings = [(""" Summary line Example - Multiline example body """, """ Summary line Example - Multiline example body """), ################################ (""" Summary line Example -- Multiline example body """, """ Summary line .. rubric:: Example Multiline example body """), ################################ (""" Summary line Example ------- Multiline example body """, """ Summary line .. rubric:: Example Multiline example body """), ################################ (""" Summary line Example ------------ Multiline example body """, """ Summary line .. rubric:: Example Multiline example body """)] for docstring, expected in docstrings: actual = str(NumpyDocstring(docstring)) assert expected == actual def test_list_in_parameter_description(self): docstring = """One line summary. Parameters ---------- no_list : int one_bullet_empty : int * one_bullet_single_line : int - first line one_bullet_two_lines : int + first line continued two_bullets_single_line : int - first line - second line two_bullets_two_lines : int * first line continued * second line continued one_enumeration_single_line : int 1. first line one_enumeration_two_lines : int 1) first line continued two_enumerations_one_line : int (iii) first line (iv) second line two_enumerations_two_lines : int a. first line continued b. second line continued one_definition_one_line : int item 1 first line one_definition_two_lines : int item 1 first line continued two_definitions_one_line : int item 1 first line item 2 second line two_definitions_two_lines : int item 1 first line continued item 2 second line continued one_definition_blank_line : int item 1 first line extra first line two_definitions_blank_lines : int item 1 first line extra first line item 2 second line extra second line definition_after_normal_text : int text line item 1 first line """ expected = """One line summary. :param no_list: :type no_list: int :param one_bullet_empty: * :type one_bullet_empty: int :param one_bullet_single_line: - first line :type one_bullet_single_line: int :param one_bullet_two_lines: + first line continued :type one_bullet_two_lines: int :param two_bullets_single_line: - first line - second line :type two_bullets_single_line: int :param two_bullets_two_lines: * first line continued * second line continued :type two_bullets_two_lines: int :param one_enumeration_single_line: 1. first line :type one_enumeration_single_line: int :param one_enumeration_two_lines: 1) first line continued :type one_enumeration_two_lines: int :param two_enumerations_one_line: (iii) first line (iv) second line :type two_enumerations_one_line: int :param two_enumerations_two_lines: a. first line continued b. second line continued :type two_enumerations_two_lines: int :param one_definition_one_line: item 1 first line :type one_definition_one_line: int :param one_definition_two_lines: item 1 first line continued :type one_definition_two_lines: int :param two_definitions_one_line: item 1 first line item 2 second line :type two_definitions_one_line: int :param two_definitions_two_lines: item 1 first line continued item 2 second line continued :type two_definitions_two_lines: int :param one_definition_blank_line: item 1 first line extra first line :type one_definition_blank_line: int :param two_definitions_blank_lines: item 1 first line extra first line item 2 second line extra second line :type two_definitions_blank_lines: int :param definition_after_normal_text: text line item 1 first line :type definition_after_normal_text: int """ config = Config(napoleon_use_param=True) actual = str(NumpyDocstring(docstring, config)) assert expected == actual expected = """One line summary. :Parameters: * **no_list** (:class:`int`) * **one_bullet_empty** (:class:`int`) -- * * **one_bullet_single_line** (:class:`int`) -- - first line * **one_bullet_two_lines** (:class:`int`) -- + first line continued * **two_bullets_single_line** (:class:`int`) -- - first line - second line * **two_bullets_two_lines** (:class:`int`) -- * first line continued * second line continued * **one_enumeration_single_line** (:class:`int`) -- 1. first line * **one_enumeration_two_lines** (:class:`int`) -- 1) first line continued * **two_enumerations_one_line** (:class:`int`) -- (iii) first line (iv) second line * **two_enumerations_two_lines** (:class:`int`) -- a. first line continued b. second line continued * **one_definition_one_line** (:class:`int`) -- item 1 first line * **one_definition_two_lines** (:class:`int`) -- item 1 first line continued * **two_definitions_one_line** (:class:`int`) -- item 1 first line item 2 second line * **two_definitions_two_lines** (:class:`int`) -- item 1 first line continued item 2 second line continued * **one_definition_blank_line** (:class:`int`) -- item 1 first line extra first line * **two_definitions_blank_lines** (:class:`int`) -- item 1 first line extra first line item 2 second line extra second line * **definition_after_normal_text** (:class:`int`) -- text line item 1 first line """ config = Config(napoleon_use_param=False, napoleon_preprocess_types=True) actual = str(NumpyDocstring(docstring, config)) assert expected == actual def test_token_type(self): tokens = ( ("1", "literal"), ("-4.6", "literal"), ("2j", "literal"), ("'string'", "literal"), ('"another_string"', "literal"), ("{1, 2}", "literal"), ("{'va{ue', 'set'}", "literal"), ("optional", "control"), ("default", "control"), (", ", "delimiter"), (" of ", "delimiter"), (" or ", "delimiter"), (": ", "delimiter"), ("True", "obj"), ("None", "obj"), ("name", "obj"), (":py:class:`Enum`", "reference"), ) for token, expected in tokens: actual = _token_type(token) assert expected == actual def test_tokenize_type_spec(self): specs = ( "str", "defaultdict", "int, float, or complex", "int or float or None, optional", "list of list of int or float, optional", "tuple of list of str, float, or int", '{"F", "C", "N"}', "{'F', 'C', 'N'}, default: 'F'", "{'F', 'C', 'N or C'}, default 'F'", "str, default: 'F or C'", "int, default: None", "int, default None", "int, default :obj:`None`", '"ma{icious"', r"'with \'quotes\''", ) tokens = ( ["str"], ["defaultdict"], ["int", ", ", "float", ", or ", "complex"], ["int", " or ", "float", " or ", "None", ", ", "optional"], ["list", " of ", "list", " of ", "int", " or ", "float", ", ", "optional"], ["tuple", " of ", "list", " of ", "str", ", ", "float", ", or ", "int"], ["{", '"F"', ", ", '"C"', ", ", '"N"', "}"], ["{", "'F'", ", ", "'C'", ", ", "'N'", "}", ", ", "default", ": ", "'F'"], ["{", "'F'", ", ", "'C'", ", ", "'N or C'", "}", ", ", "default", " ", "'F'"], ["str", ", ", "default", ": ", "'F or C'"], ["int", ", ", "default", ": ", "None"], ["int", ", ", "default", " ", "None"], ["int", ", ", "default", " ", ":obj:`None`"], ['"ma{icious"'], [r"'with \'quotes\''"], ) for spec, expected in zip(specs, tokens): actual = _tokenize_type_spec(spec) assert expected == actual def test_recombine_set_tokens(self): tokens = ( ["{", "1", ", ", "2", "}"], ["{", '"F"', ", ", '"C"', ", ", '"N"', "}", ", ", "optional"], ["{", "'F'", ", ", "'C'", ", ", "'N'", "}", ", ", "default", ": ", "None"], ["{", "'F'", ", ", "'C'", ", ", "'N'", "}", ", ", "default", " ", "None"], ) combined_tokens = ( ["{1, 2}"], ['{"F", "C", "N"}', ", ", "optional"], ["{'F', 'C', 'N'}", ", ", "default", ": ", "None"], ["{'F', 'C', 'N'}", ", ", "default", " ", "None"], ) for tokens_, expected in zip(tokens, combined_tokens): actual = _recombine_set_tokens(tokens_) assert expected == actual def test_recombine_set_tokens_invalid(self): tokens = ( ["{", "1", ", ", "2"], ['"F"', ", ", '"C"', ", ", '"N"', "}", ", ", "optional"], ["{", "1", ", ", "2", ", ", "default", ": ", "None"], ) combined_tokens = ( ["{1, 2"], ['"F"', ", ", '"C"', ", ", '"N"', "}", ", ", "optional"], ["{1, 2", ", ", "default", ": ", "None"], ) for tokens_, expected in zip(tokens, combined_tokens): actual = _recombine_set_tokens(tokens_) assert expected == actual def test_convert_numpy_type_spec(self): translations = { "DataFrame": "pandas.DataFrame", } specs = ( "", "optional", "str, optional", "int or float or None, default: None", "list of tuple of str, optional", "int, default None", '{"F", "C", "N"}', "{'F', 'C', 'N'}, default: 'N'", "{'F', 'C', 'N'}, default 'N'", "DataFrame, optional", ) converted = ( "", "*optional*", ":class:`str`, *optional*", ":class:`int` or :class:`float` or :obj:`None`, *default*: :obj:`None`", ":class:`list` of :class:`tuple` of :class:`str`, *optional*", ":class:`int`, *default* :obj:`None`", '``{"F", "C", "N"}``', "``{'F', 'C', 'N'}``, *default*: ``'N'``", "``{'F', 'C', 'N'}``, *default* ``'N'``", ":class:`pandas.DataFrame`, *optional*", ) for spec, expected in zip(specs, converted): actual = _convert_numpy_type_spec(spec, translations=translations) assert expected == actual def test_parameter_types(self): docstring = dedent("""\ Parameters ---------- param1 : DataFrame the data to work on param2 : int or float or None, optional a parameter with different types param3 : dict-like, optional a optional mapping param4 : int or float or None, optional a optional parameter with different types param5 : {"F", "C", "N"}, optional a optional parameter with fixed values param6 : int, default None different default format param7 : mapping of hashable to str, optional a optional mapping param8 : ... or Ellipsis ellipsis param9 : tuple of list of int a parameter with tuple of list of int """) expected = dedent("""\ :param param1: the data to work on :type param1: :class:`DataFrame` :param param2: a parameter with different types :type param2: :class:`int` or :class:`float` or :obj:`None`, *optional* :param param3: a optional mapping :type param3: :term:`dict-like `, *optional* :param param4: a optional parameter with different types :type param4: :class:`int` or :class:`float` or :obj:`None`, *optional* :param param5: a optional parameter with fixed values :type param5: ``{"F", "C", "N"}``, *optional* :param param6: different default format :type param6: :class:`int`, *default* :obj:`None` :param param7: a optional mapping :type param7: :term:`mapping` of :term:`hashable` to :class:`str`, *optional* :param param8: ellipsis :type param8: :obj:`... ` or :obj:`Ellipsis` :param param9: a parameter with tuple of list of int :type param9: :class:`tuple` of :class:`list` of :class:`int` """) translations = { "dict-like": ":term:`dict-like `", "mapping": ":term:`mapping`", "hashable": ":term:`hashable`", } config = Config( napoleon_use_param=True, napoleon_use_rtype=True, napoleon_preprocess_types=True, napoleon_type_aliases=translations, ) actual = str(NumpyDocstring(docstring, config)) assert expected == actual def test_token_type_invalid(self, warning): tokens = ( "{1, 2", "}", "'abc", "def'", '"ghi', 'jkl"', ) errors = ( r".+: invalid value set \(missing closing brace\):", r".+: invalid value set \(missing opening brace\):", r".+: malformed string literal \(missing closing quote\):", r".+: malformed string literal \(missing opening quote\):", r".+: malformed string literal \(missing closing quote\):", r".+: malformed string literal \(missing opening quote\):", ) for token, error in zip(tokens, errors): try: _token_type(token) finally: raw_warnings = warning.getvalue() warnings = [w for w in raw_warnings.split("\n") if w.strip()] assert len(warnings) == 1 assert re.compile(error).match(warnings[0]) warning.truncate(0) @pytest.mark.parametrize( ("name", "expected"), [ ("x, y, z", "x, y, z"), ("*args, **kwargs", r"\*args, \*\*kwargs"), ("*x, **y", r"\*x, \*\*y"), ], ) def test_escape_args_and_kwargs(self, name, expected): numpy_docstring = NumpyDocstring("") actual = numpy_docstring._escape_args_and_kwargs(name) assert actual == expected def test_pep526_annotations(self): # test class attributes annotations config = Config( napoleon_attr_annotations=True, ) actual = str(NumpyDocstring(cleandoc(PEP526NumpyClass.__doc__), config, app=None, what="class", obj=PEP526NumpyClass)) expected = """\ Sample class with PEP 526 annotations and numpy docstring .. attribute:: attr1 Attr1 description :type: int .. attribute:: attr2 Attr2 description :type: str """ print(actual) assert expected == actual @pytest.mark.sphinx('text', testroot='ext-napoleon', confoverrides={'autodoc_typehints': 'description', 'autodoc_typehints_description_target': 'all'}) def test_napoleon_and_autodoc_typehints_description_all(app, status, warning): app.build() content = (app.outdir / 'typehints.txt').read_text(encoding='utf-8') assert content == ( 'typehints\n' '*********\n' '\n' 'mypackage.typehints.hello(x, *args, **kwargs)\n' '\n' ' Parameters:\n' ' * **x** (*int*) -- X\n' '\n' ' * ***args** (*int*) -- Additional arguments.\n' '\n' ' * ****kwargs** (*int*) -- Extra arguments.\n' '\n' ' Return type:\n' ' None\n' ) @pytest.mark.sphinx('text', testroot='ext-napoleon', confoverrides={'autodoc_typehints': 'description', 'autodoc_typehints_description_target': 'documented_params'}) def test_napoleon_and_autodoc_typehints_description_documented_params(app, status, warning): app.build() content = (app.outdir / 'typehints.txt').read_text(encoding='utf-8') assert content == ( 'typehints\n' '*********\n' '\n' 'mypackage.typehints.hello(x, *args, **kwargs)\n' '\n' ' Parameters:\n' ' * **x** (*int*) -- X\n' '\n' ' * ***args** (*int*) -- Additional arguments.\n' '\n' ' * ****kwargs** (*int*) -- Extra arguments.\n' ) @pytest.mark.sphinx('html', testroot='ext-napoleon-paramtype', freshenv=True) def test_napoleon_keyword_and_paramtype(app, tmp_path): inv_file = tmp_path / 'objects.inv' inv_file.write_bytes(b'''\ # Sphinx inventory version 2 # Project: Intersphinx Test # Version: 42 # The remainder of this file is compressed using zlib. ''' + zlib.compress(b'''\ None py:data 1 none.html - list py:class 1 list.html - int py:class 1 int.html - ''')) # NoQA: W291 app.config.intersphinx_mapping = {'python': ('127.0.0.1:5555', str(inv_file))} normalize_intersphinx_mapping(app, app.config) load_mappings(app) app.build(force_all=True) etree = etree_parse(app.outdir / 'index.html') for name, typename in product(('keyword', 'kwarg', 'kwparam'), ('paramtype', 'kwtype')): param = f'{name}_{typename}' li_ = list(etree.findall(f'.//li/p/strong[.="{param}"]/../..')) assert len(li_) == 1 li = li_[0] text = li.text or ''.join(li.itertext()) assert text == f'{param} (list[int]) \u2013 some param' a_ = list(li.findall('.//a[@class="reference external"]')) assert len(a_) == 2 for a, uri in zip(a_, ('list.html', 'int.html')): assert a.attrib['href'] == f'127.0.0.1:5555/{uri}' assert a.attrib['title'] == '(in Intersphinx Test v42)'