"""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, validate_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'))): # NoQA: PYI024 """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 = 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 str(actual) == expected 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 = 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 str(actual) == expect # Single line actual = GoogleDocstring(f'{section}:\n this is a single line\n', config) expect = f'.. {admonition}:: this is a single line\n' assert str(actual) == expect 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 = GoogleDocstring(dedent(docstring), config) expected = dedent(expected) assert str(actual) == expected 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 = 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 str(actual) == expected def test_attributes_with_class_reference(self): docstring = """\ Attributes: in_attr(:class:`numpy.ndarray`): super-dooper attribute """ actual = GoogleDocstring(docstring) expected = """\ .. attribute:: in_attr super-dooper attribute :type: :class:`numpy.ndarray` """ assert str(actual) == expected docstring = """\ Attributes: in_attr(numpy.ndarray): super-dooper attribute """ actual = 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 = GoogleDocstring(docstring, config, obj=self.__class__) expected = """\ :ivar foo: blah blah :vartype foo: int :ivar bar: blah blah :vartype bar: str """ assert str(actual) == expected def test_code_block_in_returns_section(self): docstring = """ Returns: foobar: foo:: codecode codecode """ expected = """ :returns: foo:: codecode codecode :rtype: foobar """ actual = GoogleDocstring(docstring) assert str(actual) == expected 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 = GoogleDocstring(docstring) assert str(actual) == expected 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 = GoogleDocstring(docstring) assert str(actual) == expected 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 = GoogleDocstring(docstring) assert str(actual) == expected 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 = GoogleDocstring(docstring) assert str(actual) == expected 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 = GoogleDocstring(docstring) assert str(actual) == expected 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 = GoogleDocstring(docstring, config) assert str(actual) == expected 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 = GoogleDocstring(docstring, config) assert str(actual) == expected 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 = GoogleDocstring(docstring, testConfig) assert str(actual) == expected 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 = GoogleDocstring( docstring, config=config, app=None, what='module', options={'no-index': True}, ) assert str(actual) == expected def test_keywords_with_types(self): docstring = """\ Do as you please Keyword Args: gotham_is_yours (None): shall interfere. """ actual = GoogleDocstring(docstring) expected = """\ Do as you please :keyword gotham_is_yours: shall interfere. :kwtype gotham_is_yours: None """ assert str(actual) == expected def test_pep526_annotations(self): # Test class attributes annotations config = Config( napoleon_attr_annotations=True, ) actual = 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 str(actual) == expected def test_preprocess_types(self): docstring = """\ Do as you please Yield: str:Extended """ actual = GoogleDocstring(docstring) expected = """\ Do as you please :Yields: *str* -- Extended """ assert str(actual) == expected config = Config(napoleon_preprocess_types=True) actual = GoogleDocstring(docstring, config) expected = """\ Do as you please :Yields: :py:class:`str` -- Extended """ assert str(actual) == expected 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 underline = '-' * len(section) actual = NumpyDocstring( f'{section}\n' f'{underline}\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 str(actual) == expect # Single line actual = NumpyDocstring( f'{section}\n{"-" * len(section)}\n this is a single line\n', config, ) expect = f'.. {admonition}:: this is a single line\n' assert str(actual) == expect 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 = NumpyDocstring(dedent(docstring), config) expected = dedent(expected) assert str(actual) == expected 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 = NumpyDocstring(docstring, config) expected = dedent(""" Single line summary :Parameters: **arg1** (*str*) -- Extended description of arg1 """) assert str(actual) == expected 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 = NumpyDocstring(docstring, config) expected = """\ :Parameters: **param1** (:class:`MyClass ` instance) :Other Parameters: **param2** (:class:`MyClass ` instance) """ assert str(actual) == expected config = Config(napoleon_use_param=True) actual = NumpyDocstring(docstring, config) expected = """\ :param param1: :type param1: :class:`MyClass ` instance :param param2: :type param2: :class:`MyClass ` instance """ assert str(actual) == expected def test_multiple_parameters(self): docstring = """\ Parameters ---------- x1, x2 : array_like Input arrays, description of ``x1``, ``x2``. """ config = Config(napoleon_use_param=False) actual = NumpyDocstring(docstring, config) expected = """\ :Parameters: **x1, x2** (*array_like*) -- Input arrays, description of ``x1``, ``x2``. """ assert str(actual) == expected config = Config(napoleon_use_param=True) actual = 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 str(actual) == expected def test_parameters_without_class_reference(self): docstring = """\ Parameters ---------- param1 : MyClass instance """ config = Config(napoleon_use_param=False) actual = NumpyDocstring(docstring, config) expected = """\ :Parameters: **param1** (*MyClass instance*) """ assert str(actual) == expected config = Config(napoleon_use_param=True) actual = NumpyDocstring(dedent(docstring), config) expected = """\ :param param1: :type param1: MyClass instance """ assert str(actual) == expected def test_see_also_refs(self): docstring = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) See Also -------- some, other, funcs otherfunc : relationship """ actual = NumpyDocstring(docstring) expected = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) .. seealso:: :obj:`some`, :obj:`other`, :obj:`funcs` \n\ :obj:`otherfunc` relationship """ assert str(actual) == expected docstring = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) See Also -------- some, other, funcs otherfunc : relationship """ config = Config() app = mock.Mock() actual = 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 str(actual) == expected 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 = 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 str(actual) == expected 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 = NumpyDocstring(docstring, config, app, 'method') assert str(actual) == expected 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 = NumpyDocstring(docstring, config, app, 'class') assert str(actual) == expected 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 = NumpyDocstring(docstring, config, app, 'class') assert str(actual) == expected 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 = NumpyDocstring(docstring, config) assert str(actual) == expected 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 = NumpyDocstring(docstring, config, app, 'method') assert str(actual) == expected 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 = NumpyDocstring(docstring, config, app, 'method') assert str(actual) == expected 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 = NumpyDocstring(docstring, config, app, 'method') assert str(actual) == expected 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 = NumpyDocstring(docstring) assert str(actual) == expected 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 = NumpyDocstring(docstring, config) assert str(actual) == expected 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 = NumpyDocstring(docstring, config) assert str(actual) == expected 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 actual == expected 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\''"], ) # fmt: skip for spec, expected in zip(specs, tokens, strict=True): actual = _tokenize_type_spec(spec) assert actual == expected 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, strict=True): actual = _recombine_set_tokens(tokens_) assert actual == expected 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, strict=True): actual = _recombine_set_tokens(tokens_) assert actual == expected 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, strict=True): actual = _convert_numpy_type_spec(spec, translations=translations) assert actual == expected 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 = NumpyDocstring(docstring, config) assert str(actual) == expected @pytest.mark.sphinx('html', testroot='root') def test_token_type_invalid(self, app): 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, strict=True): try: _token_type(token) finally: raw_warnings = app.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]) app.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 = 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 str(actual) == expected @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): 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): 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))} validate_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'), strict=True): assert a.attrib['href'] == f'127.0.0.1:5555/{uri}' assert a.attrib['title'] == '(in Intersphinx Test v42)'