
"""
Forked from the tests for ``sphinx.ext.napoleon.docstring`` module.
:copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import re
from typing import Type, Union, Any
from unittest import TestCase
from textwrap import dedent
import functools

from pydoctor.napoleon.docstring import (GoogleDocstring as _GoogleDocstring, 
        NumpyDocstring as _NumpyDocstring, 
        TokenType, TypeDocstring, is_type, is_google_typed_arg)

import sphinx.ext.napoleon as sphinx_napoleon

__docformat__ = "restructuredtext"

def partialclass(cls: Type[Any], *args: Any, **kwds: Any) -> Type[Any]:
    # mypy gets errors: - Variable "cls" is not valid as a type
    #                   - Invalid base class "cls" 
    class NewCls(cls): #type: ignore
        __init__ = functools.partialmethod(cls.__init__, *args, **kwds) #type: ignore
        __class__ = cls
    assert isinstance(NewCls, type)
    return NewCls

sphinx_napoleon_config = sphinx_napoleon.Config(
    napoleon_use_admonition_for_examples=True, 
    napoleon_use_admonition_for_notes=True,
    napoleon_use_admonition_for_references=True,
    napoleon_use_ivar=True,
    napoleon_use_param=True,
    napoleon_use_keyword=True,
    napoleon_use_rtype=True,
    napoleon_preprocess_types=True)

# Adapters for upstream Sphinx napoleon classes
SphinxGoogleDocstring = partialclass(sphinx_napoleon.docstring.GoogleDocstring, 
    config=sphinx_napoleon_config, what='function')
SphinxNumpyDocstring = partialclass(sphinx_napoleon.docstring.NumpyDocstring, 
    config=sphinx_napoleon_config, what='function')

# Create adapter classes that uses process_type_fields=True for the testing purposes
GoogleDocstring = partialclass(_GoogleDocstring, process_type_fields=True)
NumpyDocstring = partialclass(_NumpyDocstring, process_type_fields=True)

class BaseDocstringTest(TestCase):
    maxDiff = None

    # mypy get error:
    # Variable "pydoctor.test.test_napoleon_docstring.SphinxGoogleDocstring" is not valid as a type 
    def assertAlmostEqualSphinxDocstring(self, expected: str, docstring: str, 
                                         type_: Type[Union[SphinxGoogleDocstring, SphinxNumpyDocstring]]) -> None: #type: ignore[valid-type]
        """
        Check if the upstream version of the parser class (from `sphinx.ext.napoleon`) 
        parses the docstring as expected.

        This is used as a supplementary manner of testing the parser behaviour.
        
        Some approximation are applied with `re.sub` to the ``expected`` string and the reST 
        docstring generated by `sphinx.ext.napoleon` classes. This is done in order to use 
        the expected reST strings designed for `pydoctor.napoleon` and apply them to `sphinx.ext.napoleon` in the same test. 
        
        Tho, not all tests cases can be adapted to pass this check. 

        :param expected: The exact expected reST docstring generated by `pydoctor.napoleon` classes (trailling whitespaces ignored)
        """
        expected_sphinx_output = re.sub(
                r"(`|\\\s|\\|:mod:|:func:|:class:|:obj:)", "", expected)

        # mypy error: Cannot instantiate type "Type[SphinxGoogleDocstring?]
        sphinx_docstring_output = re.sub(
            r"(`|\\|:mod:|:func:|:class:|:obj:|\s<Ellipsis>)", "",
            str(type_(docstring)).replace( #type: ignore[misc]
            ":kwtype", ":type").replace(":vartype", ":type").replace(" -- ", " - ").replace(':rtype:', ':returntype:').rstrip())

        self.assertEqual(expected_sphinx_output.rstrip(), sphinx_docstring_output)
    
class TypeDocstringTest(BaseDocstringTest):

    def test_is_type(self):

        self.assertFalse(is_type("Random words are not a type spec"))
        self.assertFalse(is_type("List of string or any kind fo sequences of strings"))

        self.assertTrue(is_type("Sequence(str), optional"))
        self.assertTrue(is_type("Sequence(str) or str"))
        self.assertTrue(is_type("List[str] or list(bytes), optional"))
        self.assertTrue(is_type('{"F", "C", "N"}, optional'))
        self.assertTrue(is_type("list of int or float or None, default: None"))
        self.assertTrue(is_type("`complicated string` or `strIO <twisted.python.compat.NativeStringIO>`"))
    
    def test_is_google_typed_arg(self):

        self.assertFalse(is_google_typed_arg("Random words are not a type spec"))
        self.assertFalse(is_google_typed_arg("List of string or any kind fo sequences of strings"))

        self.assertTrue(is_google_typed_arg("Sequence(str), optional"))
        self.assertTrue(is_google_typed_arg("Sequence(str) or str"))
        self.assertTrue(is_google_typed_arg("List[str] or list(bytes), optional"))
        self.assertTrue(is_google_typed_arg('{"F", "C", "N"}, optional'))
        self.assertTrue(is_google_typed_arg("list of int or float or None, default: None"))
        self.assertTrue(is_google_typed_arg("`complicated string` or `strIO <twisted.python.compat.NativeStringIO>`"))

        # Google-style specific

        self.assertFalse(is_google_typed_arg("foo (Random words are not a type spec)"))
        self.assertFalse(is_google_typed_arg("foo (List of string or any kind fo sequences of strings)"))

        self.assertTrue(is_google_typed_arg("foo (Sequence(str), optional)"))
        self.assertTrue(is_google_typed_arg("foo (Sequence[str] or str)"))
        self.assertTrue(is_google_typed_arg("foo (List[str] or list(bytes), optional)"))
        self.assertTrue(is_google_typed_arg('foo ({"F", "C", "N"}, optional)'))
        self.assertTrue(is_google_typed_arg("foo (list of int or float or None, default: None)"))
        self.assertTrue(is_google_typed_arg("foo (`complicated string` or `strIO <twisted.python.compat.NativeStringIO>`)"))

        self.assertTrue(is_google_typed_arg("Random words are not a type spec (List[str] or list(bytes), optional)"))
        self.assertTrue(is_google_typed_arg("Random words are not a type spec (list of int or float or None, default: None)"))
        self.assertTrue(is_google_typed_arg("Random words are not a type spec (`complicated string` or `strIO <twisted.python.compat.NativeStringIO>`, optional)"))

    def test_token_type(self):
        tokens = (
        ("1", TokenType.LITERAL),
        ("-4.6", TokenType.LITERAL),
        ("2j", TokenType.LITERAL),
        ("'string'", TokenType.LITERAL),
        ('"another_string"', TokenType.LITERAL),
        ("{1, 2}", TokenType.LITERAL),
        ("{'va{ue', 'set'}", TokenType.LITERAL),
        ("optional", TokenType.CONTROL),
        ("default", TokenType.CONTROL),
        (", ", TokenType.DELIMITER),
        (" of ", TokenType.DELIMITER),
        (" or ", TokenType.DELIMITER),
        (": ", TokenType.DELIMITER),
        ("]", TokenType.DELIMITER),
        ("[", TokenType.DELIMITER),
        (")", TokenType.DELIMITER),
        ("(", TokenType.DELIMITER),
        ("True", TokenType.OBJ),
        ("None", TokenType.OBJ),
        ("name", TokenType.OBJ),
        (":py:class:`Enum`", TokenType.REFERENCE),
        ("`a complicated string`",  TokenType.REFERENCE),
        ("just a string",  TokenType.UNKNOWN),
        (len("not a string"),  TokenType.ANY),
        )
        type_spec = TypeDocstring('', 0)
        for token, _type in tokens:
            actual = type_spec._token_type(token)
            self.assertEqual(_type, actual)

    def test_tokenize_type_spec(self):
        specs = (
            "str",
            "defaultdict",
            "int, float, or complex",
            "int or float or None, optional",
            '{"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"],
            ["{", '"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 = TypeDocstring._tokenize_type_spec(spec)
            self.assertEqual(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 = TypeDocstring._recombine_set_tokens(tokens_)
            self.assertEqual(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 = TypeDocstring._recombine_set_tokens(tokens_)
            self.assertEqual(expected, actual)

    def test_convert_numpy_type_spec(self):

        specs = (
            "",
            "optional",
            "str, optional",
            "int or float or None, default: None",
            "int, default None",
            '{"F", "C", "N"}',
            "{'F', 'C', 'N'}, default: 'N'",
            "{'F', 'C', 'N'}, default 'N'",
            "DataFrame, optional",
            "default[str]", # corner cases...
            "optional[str]",
            ",[str]", 
            ", [str]",
            " of [str]",
            " or [str]",
            ": [str]",
            " and [str]",
            "'hello'[str]",
            '"hello"[str]',
            "`hello`[str]",
            "`hello <https://github.com>`_[str]",
            "**hello**[str]",
        )

        converted = (
            "",
            "*optional*",
            "`str`, *optional*",
            "`int` or `float` or `None`, *default*: `None`",
            "`int`, *default* `None`",
            '``{"F", "C", "N"}``',
            "``{'F', 'C', 'N'}``, *default*: ``'N'``",
            "``{'F', 'C', 'N'}``, *default* ``'N'``",
            "`DataFrame`, *optional*",
            r"*default*\ [`str`]",
            r"*optional*\ [`str`]",
            ", [`str`]",
            ", [`str`]",
            " of [`str`]",
            " or [`str`]",
            ": [`str`]",
            " and [`str`]",
            r"``'hello'``\ [`str`]",
            r'``"hello"``\ [`str`]',
            r"`hello`\ [`str`]",
            r"`hello <https://github.com>`_\ [`str`]",
            r"**hello**\ [`str`]",
        )

        for spec, expected in zip(specs, converted):
            actual = str(TypeDocstring(spec))
            self.assertEqual(expected, actual)

    def test_token_type_invalid(self):
        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):
            type_spec = TypeDocstring('')
            type_spec._token_type(token)
            match_re = re.compile(error)
            assert len(type_spec.warnings) == 1, type_spec.warnings
            assert match_re.match(str(type_spec.warnings.pop()))

    def test_unbalanced_parenthesis(self):
        strings = (
            "list[union[str, bytes]",
            "list(union[str, bytes)",
            "list[union(str, bytes]",
        )
        errors = (
            r"unbalanced square braces",
            r"unbalanced square braces",
            r"unbalanced parenthesis",
        )
        for string, error in zip(strings, errors):
            type_spec = TypeDocstring(string)

            match_re = re.compile(error)
            assert len(type_spec.warnings) == 1, type_spec.warnings
            assert match_re.match(str(type_spec.warnings.pop()))


class InlineAttributeTest(BaseDocstringTest):

    def test_class_data_member(self):
        docstring = """\
data member description:
- a: b
"""
        actual = str(GoogleDocstring(docstring, is_attribute=True))
        expected = """\
data member description:
- a: b"""   

        self.assertEqual(expected.rstrip(), actual)

    def test_class_data_member_inline(self):
        docstring = """b: data member description with :ref:`reference`"""
        actual = str(GoogleDocstring(docstring, is_attribute=True))
        expected = ("""\
data member description with :ref:`reference`

:type: `b`""")
        self.assertEqual(expected.rstrip(), actual)

    def test_class_data_member_inline_no_type(self):
        docstring = """data with ``a : in code`` and :ref:`reference` and no type"""
        actual = str(GoogleDocstring(docstring, is_attribute=True))
        expected = """data with ``a : in code`` and :ref:`reference` and no type"""

        self.assertEqual(expected.rstrip(), actual)

    def test_class_data_member_inline_ref_in_type(self):
        docstring = """:class:`int`: data member description"""
        actual = str(GoogleDocstring(docstring, is_attribute=True))
        expected = ("""\
data member description

:type: :class:`int`""")
        self.assertEqual(expected.rstrip(), actual)


class GoogleDocstringTest(BaseDocstringTest):
    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

:param arg1: Extended
             description of arg1
:type arg1: `str`
"""
    ), (
        """
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

:param arg1: Extended
             description of arg1
:type arg1: `str`
:param arg2: Extended
             description of arg2
:type arg2: `int`

:keyword kwarg1: Extended
                 description of kwarg1
:type kwarg1: `str`
:keyword kwarg2: Extended
                 description of kwarg2
:type kwarg2: `int`
"""
    ), (
        """
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

:param arg1: Extended
             description of arg1
:type arg1: `str`
:param arg2: Extended
             description of arg2
:type arg2: `int`

:keyword kwarg1: Extended
                 description of kwarg1
:type kwarg1: `str`
:keyword kwarg2: Extended
                 description of kwarg2
:type kwarg2: `int`
        """
    ), (
        """
Single line summary

Return:
    str:Extended
    description of return value
""",
"""
Single line summary

:returns: Extended
          description of return value
:returntype: `str`
"""
    ), (
        """
Single line summary

Returns:
    str:Extended
    description of return value
""",
"""
Single line summary

:returns: Extended
          description of return value
:returntype: `str`
"""
    ), (
        """
Single line summary

Returns:
    Extended
    description of return value
""",
"""
Single line summary

:returns: Extended
          description of return value
"""
    ), (
        """
Single line summary

Args:
    arg1(str):Extended
        description of arg1
    *args: Variable length argument list.
    **kwargs: Arbitrary keyword arguments.
""",
r"""
Single line summary

:param arg1: Extended
             description of arg1
:type arg1: `str`
:param \*args: Variable length argument list.
:param \*\*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
""",
r"""
Single line summary

:param arg1: Description
:type arg1: `list`\ (`int`)
:param arg2: Description
:type arg2: `list`\ [`int`]
:param arg3: Description
:type arg3: `dict`\ (`str`, `int`)
:param arg4: Description
:type arg4: `dict`\ [`str`, `int`]
"""
    ), (
        """
Single line summary

Receive:
    arg1 (list(int)): Description
    arg2 (list[int]): Description
""",
r"""
Single line summary

:param arg1: Description
:type arg1: `list`\ (`int`)
:param arg2: Description
:type arg2: `list`\ [`int`]
"""
    ), (
        """
Single line summary

Receives:
    arg1 (list(int)): Description
    arg2 (list[int]): Description
""",
r"""
Single line summary

:param arg1: Description
:type arg1: `list`\ (`int`)
:param arg2: Description
:type arg2: `list`\ [`int`]
"""
    ), (
        """
Single line summary

Yield:
    str:Extended
    description of yielded value
""",
"""
Single line summary

:yields: Extended
         description of yielded value
:yieldtype: `str`
"""
    ), (
        """
Single line summary

Yields:
    Extended
    description of yielded value
""",
"""
Single line summary

:yields: Extended
         description of yielded value
"""
    ), (
        """
Single line summary

Args:
    arg1 (list(int)):
        desc arg1. 
    arg2 (list[int]):
        desc arg2.
""",
r"""
Single line summary

:param arg1: desc arg1.
:type arg1: `list`\ (`int`)
:param arg2: desc arg2.
:type arg2: `list`\ [`int`]
"""
    ),(
        """
Single line summary

Args:
    my first argument (list(int)):
        desc arg1. 
    my second argument (list[int]):
        desc arg2.
""",
r"""
Single line summary

:param my first argument: desc arg1.
:type my first argument: `list`\ (`int`)
:param my second argument: desc arg2.
:type my second argument: `list`\ [`int`]
"""
    ), ("""
Single line summary

Usage:
    import stuff
    stuff.do()
""", # nothing special about the headings that are not recognized as a section
"""
Single line summary

Usage:
    import stuff
    stuff.do()"""),(
"""
Single line summary

Todo:
    stuff
""",
"""
Single line summary

.. admonition:: Todo

   stuff
"""
    ),(
"""
Single line summary

Todo:

""",
"""
Single line summary

Todo:
"""),("""
Single line summary

References:
    stuff
""",
"""
Single line summary

.. admonition:: References

   stuff
"""),("""
Single line summary

See also:
    my thing
""",
"""
Single line summary

.. seealso:: my thing
""")]

    def test_docstrings(self):
        for docstring, expected in self.docstrings:
            actual = str(GoogleDocstring(docstring))
            self.assertEqual(expected.rstrip(), actual)
            if not 'Yield' in docstring and not 'Todo' in docstring: # The yield and todo sections are very different from sphinx's.
                self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxGoogleDocstring)

    def test_returns_section_type_only(self):

        docstring="""
Single line summary

Returns:
    str:
"""
        expected="""
Single line summary

:returns: str 
"""
        actual = str(GoogleDocstring(docstring))

        self.assertEqual(expected.strip(), actual.strip())

        docstring="""
Single line summary

Returns:
    str
"""
        expected="""
Single line summary

:returns: str 
"""
        actual = str(GoogleDocstring(docstring))

        self.assertEqual(expected.strip(), actual.strip())

    def test_sphinx_admonitions(self):
        admonition_map = {
            'Attention': 'attention',
            'Caution': 'caution',
            'Danger': 'danger',
            'Error': 'error',
            'Hint': 'hint',
            'Important': 'important',
            'Note': 'note',
            'Tip': 'tip',
            'Warning': 'warning',
            'Warnings': 'warning',
        }

        for section, admonition in admonition_map.items():
            # Multiline
            actual = str(GoogleDocstring(("{}:\n"
                                          "    this is the first line\n"
                                          "\n"
                                          "    and this is the second line\n"
                                          ).format(section)))
            expect = (".. {}::\n"
                      "\n"
                      "   this is the first line\n"
                      "   \n"
                      "   and this is the second line\n"
                      ).format(admonition)
            self.assertEqual(expect.rstrip(), actual)

            # Single line
            actual = str(GoogleDocstring(("{}:\n"
                                          "    this is a single line\n"
                                          ).format(section)))
            expect = (".. {}:: this is a single line\n"
                      ).format(admonition)
            self.assertEqual(expect.rstrip(), actual)


    def test_parameters_with_class_reference(self):
        # mot sure why this test include back slash in the type spec...
        # users should not write type like that in pydoctor anyway.
        docstring = r"""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 = r"""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`
"""
        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring,
            type_=SphinxGoogleDocstring)

    def test_attributes_with_class_reference(self):
        docstring = """\
Attributes:
    in_attr(:class:`numpy.ndarray`): super-dooper attribute
"""

        actual = str(GoogleDocstring(docstring))
        expected = """\
:ivar in_attr: super-dooper attribute
:type in_attr: :class:`numpy.ndarray`
"""
        self.assertEqual(expected.rstrip(), actual)

        docstring = """\
Attributes:
    in_attr(numpy.ndarray): super-dooper attribute
"""

        actual = str(GoogleDocstring(docstring))
        expected = """\
:ivar in_attr: super-dooper attribute
:type in_attr: `numpy.ndarray`
"""
        self.assertEqual(expected.rstrip(), actual)

    def test_code_block_in_returns_section(self):
        docstring = """
Returns:
    foobar: foo::
        codecode
        codecode
"""
        expected = """
:returns:
          foo::
              codecode
              codecode
:returntype: `foobar`
"""
        actual = str(GoogleDocstring(docstring))
        self.assertEqual(expected.rstrip(), 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.
:returntype: :py:class:`~.module.submodule.SomeClass`
"""
        actual = str(GoogleDocstring(docstring))
        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring,
            type_=SphinxGoogleDocstring)

    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
:returntype: :class:`numpy.ndarray`
"""
        actual = str(GoogleDocstring(docstring))
        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring,
            type_=SphinxGoogleDocstring)

    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))
            self.assertEqual(expected.rstrip(), actual)
            self.assertAlmostEqualSphinxDocstring(expected, docstring,
                type_=SphinxGoogleDocstring)

    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))
        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring,
            type_=SphinxGoogleDocstring)

    def test_section_header_formatting(self):
        docstrings = [("""
Summary line

Example:
    Multiline reStructuredText
    literal code block
""", """
Summary line

.. admonition:: 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))
            self.assertEqual(expected.rstrip(), actual)
            self.assertAlmostEqualSphinxDocstring(expected, docstring,
                type_=SphinxGoogleDocstring)

    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`
"""
        actual = str(GoogleDocstring(docstring))
        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring,
            type_=SphinxGoogleDocstring)


    def test_attr_with_method(self):
        docstring = """
Attributes:
    arg : description

Methods:
    func(abc, def): description
"""

        expected = r"""
:ivar arg: description

.. admonition:: Methods

   `func`\ (`abc`, `def`)
       description
"""  # NOQA

        actual = str(GoogleDocstring(docstring))
        self.assertEqual(expected.rstrip(), actual)

    def test_return_formatting_indentation(self):

        docstring = """
Returns:
    bool: True if successful, False otherwise.

    The return type is optional and may be specified at the beginning of
    the ``Returns`` section followed by a colon.

    The ``Returns`` section may span multiple lines and paragraphs.
    Following lines should be indented to match the first line.

    The ``Returns`` section supports any reStructuredText formatting,
    including literal blocks::

        {
            'param1': param1,
            'param2': param2
        }
"""

        expected = """
:returns: True if successful, False otherwise.

          The return type is optional and may be specified at the beginning of
          the ``Returns`` section followed by a colon.

          The ``Returns`` section may span multiple lines and paragraphs.
          Following lines should be indented to match the first line.

          The ``Returns`` section supports any reStructuredText formatting,
          including literal blocks::

              {
                  'param1': param1,
                  'param2': param2
              }
:returntype: `bool`
""" 

        actual = str(GoogleDocstring(docstring))
        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring,
            type_=SphinxGoogleDocstring)

    def test_column_summary_lines_sphinx_issue_4016(self):
        # test https://github.com/sphinx-doc/sphinx/issues/4016

        docstring = """Get time formated as ``HH:MM:SS``."""

        expected = """Get time formated as ``HH:MM:SS``."""

        actual = str(GoogleDocstring(docstring))
        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring,
            type_=SphinxGoogleDocstring)

        actual = str(GoogleDocstring(docstring, is_attribute=True))
        self.assertEqual(expected.rstrip(), actual)

        docstring2 = """Put *key* and *value* into a dictionary.

Returns:
    A dictionary ``{key: value}``
"""
        expected2 = """Put *key* and *value* into a dictionary.

:returns: A dictionary ``{key: value}``
"""

        actual = str(GoogleDocstring(docstring2))
        self.assertEqual(expected2.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected2, docstring2,
            type_=SphinxGoogleDocstring)

        actual = str(GoogleDocstring(docstring2, is_attribute=True))
        self.assertEqual(expected2.rstrip(), actual)

    def test_multiline_types(self):

        # Real life example from 
        # https://googleapis.github.io/google-api-python-client/docs/epy/index.html
        docstring = """
Scopes the credentials if necessary.

Args:
    credentials (Union[
        google.auth.credentials.Credentials,
        oauth2client.client.Credentials]): The
            credentials to scope.
    scopes (Sequence[str]): The list of scopes.
    errors (Sequence[Union[ParseError,
        ParseWarning, ParseInfo, ...]]): The list of errors,
        warnings or other informations.

Returns:
    Union[google.auth.credentials.Credentials,
        oauth2client.client.Credentials]: The scoped credentials.
"""

        expected = r"""
Scopes the credentials if necessary.

:param credentials: The
                    credentials to scope.
:type credentials: `Union`\ [`google.auth.credentials.Credentials`, `oauth2client.client.Credentials`]
:param scopes: The list of scopes.
:type scopes: `Sequence`\ [`str`]
:param errors: The list of errors,
               warnings or other informations.
:type errors: `Sequence`\ [`Union`\ [`ParseError`, `ParseWarning`, `ParseInfo`, `...`]]

:returns: The scoped credentials.
:returntype: `Union`\ [`google.auth.credentials.Credentials`, `oauth2client.client.Credentials`]
"""
        actual = str(GoogleDocstring(docstring))
        self.assertEqual(expected.rstrip(), actual)
    
    def test_multiline_types_invalid_log_warning(self):
        # test robustness with invalid arg syntax + log warning
        docstring = """
Description...

Args:
    docformat
        Can be one of:
        - "numpy"
        - "google"
    scopes (Sequence[str]): The list of scopes.
"""

        expected = r"""
Description...

:param docformat: Can be one of:
                  - "numpy"
                  - "google"
:param scopes: The list of scopes.
:type scopes: `Sequence`\ [`str`]
"""     
        doc = GoogleDocstring(docstring)
        actual = str(doc)
        self.assertEqual(expected.rstrip(), actual)
        self.assertEqual(1, len(doc.warnings))
        warning = doc.warnings.pop()
        self.assertIn("invalid type: 'docformatCan be one of'", warning[0])
        self.assertEqual(5, warning[1])

        docstring = """
Description...

Args:
    docformat (Can be "numpy" 
        or "google"): Desc
    scopes (Sequence[str]): The list of scopes.
"""

        expected = r"""
Description...

:param docformat (Can be "numpy": or "google"): Desc
:param scopes: The list of scopes.
:type scopes: `Sequence`\ [`str`]
"""     
        doc = GoogleDocstring(docstring)
        actual = str(doc)
        self.assertEqual(expected.rstrip(), actual)
        self.assertEqual(1, len(doc.warnings))
        warning = doc.warnings.pop()
        self.assertIn("invalid type: 'docformat (Can be \"numpy\"or \"google\")'", warning[0])
        self.assertEqual(5, warning[1])

class NumpyDocstringTest(BaseDocstringTest):
    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

        :param arg1: Extended
                     description of arg1
        :type arg1: `str`
        """
    ), (
        """
        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

        :param arg1: Extended
                     description of arg1
        :type arg1: `str`
        :param arg2: Extended
                     description of arg2
        :type arg2: `int`

        :keyword kwarg1: Extended
                         description of kwarg1
        :type kwarg1: `str`
        :keyword kwarg2: Extended
                         description of kwarg2
        :type kwarg2: `int`
        """
    ), (
        """
        Single line summary

        Return
        ------
        str
            Extended
            description of return value
        """,
        """
        Single line summary

        :returns: Extended
                  description of return value
        :returntype: `str`
        """
    ),(
        """
        Single line summary

        Return
        ------
        the string of your life: str
        """,
        """
        Single line summary

        :returns: **the string of your life**
        :returntype: `str`
        """
    ),(
        """
        Single line summary

        Return
        ------
        
        """,
        """
        Single line summary
        """
    ), (
        """
        Single line summary

        Returns
        -------
        str
            Extended
            description of return value
        """,
        """
        Single line summary

        :returns: Extended
                  description of return value
        :returntype: `str`
        """
    ), (
        """
        Single line summary

        Parameters
        ----------
        arg1:str
             Extended description of arg1
        *args:
            Variable length argument list.
        **kwargs:
            Arbitrary keyword arguments.
        """,
        """
        Single line summary

        :param arg1: Extended description of arg1
        :type arg1: `str`
        :param \\*args: Variable length argument list.
        :param \\*\\*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

        :param arg1: Extended description of arg1
        :type arg1: `str`
        :param \\*args: Variable length argument list and arbitrary keyword arguments.
        :param \\*\\*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

        :param arg1: Extended
                     description of arg1
        :type arg1: `str`
        :param arg2: Extended
                     description of arg2
        :type arg2: `int`
        """
    ), (
        """
        Single line summary

        Receives
        --------
        arg1:str
            Extended
            description of arg1
        arg2 : int
            Extended
            description of arg2
        """,
        """
        Single line summary

        :param arg1: Extended
                     description of arg1
        :type arg1: `str`
        :param arg2: Extended
                     description of arg2
        :type arg2: `int`
        """
    ), (
        """
        Single line summary

        Yield
        -----
        str
            Extended
            description of yielded value
        """,
        """
        Single line summary

        :yields: Extended
                 description of yielded value
        :yieldtype: `str`
        """
    ), (
        """
        Single line summary

        Yields
        ------
        str
            Extended
            description of yielded value
        """,
        """
        Single line summary

        :yields: Extended
                 description of yielded value
        :yieldtype: `str`
        """
    ), ("""
        Derived from the NumpyDoc implementation of _parse_see_also::

            See Also
            --------
            func_name : Descriptive text
                continued text
            another_func_name : Descriptive text
            func_name1, func_name2, :meth:`func_name`, func_name3
        """, 
        """
        Derived from the NumpyDoc implementation of _parse_see_also::

            See Also
            --------
            func_name : Descriptive text
                continued text
            another_func_name : Descriptive text
            func_name1, func_name2, :meth:`func_name`, func_name3
        """),(
        """
        Single line summary

        Args
        ----
        my first argument: list(int)
            desc arg1. 
        my second argument: list[int]
            desc arg2.
        """,
        r"""
        Single line summary

        :param my first argument: desc arg1.
        :type my first argument: `list`\ (`int`)
        :param my second argument: desc arg2.
        :type my second argument: `list`\ [`int`]
        """),("""
Single line summary

Usage
-----
import stuff
stuff.do()
""", """
Single line summary

Usage
-----
import stuff
stuff.do()
"""),
("""
Single line summary

Generic admonition
------------------
""", # nothing special about the headings that are not recognized as a section
"""
Single line summary

Generic admonition
------------------
"""),(
        """
        Single line summary

        Todo
        ----
        stuff
        
        """,
        """
        Single line summary

        .. admonition:: Todo

           stuff
        """),(
        """
        Single line summary

        Todo
        ----
        """,
        """
        Single line summary

        .. admonition:: Todo
        """)
        ,(
        """
        Single line summary

        References
        ----------
        stuff
        """,
        """
        Single line summary

        .. admonition:: References

           stuff
        """)
]

    def test_docstrings(self):
    
        for docstring, expected in self.docstrings:
            actual = str(NumpyDocstring(dedent(docstring)))
            expected = dedent(expected)
            self.assertEqual(actual, expected.rstrip())
            if not 'Yield' in docstring and not 'Todo' in docstring: # The yield and todo sections are very different from sphinx's.
                self.assertAlmostEqualSphinxDocstring(expected, dedent(docstring), type_=SphinxNumpyDocstring)

    def test_sphinx_admonitions(self):
        admonition_map = {
            'Attention': 'attention',
            'Caution': 'caution',
            'Danger': 'danger',
            'Error': 'error',
            'Hint': 'hint',
            'Important': 'important',
            'Note': 'note',
            'Tip': 'tip',
            'Warning': 'warning',
            'Warnings': 'warning',
        }

        for section, admonition in admonition_map.items():
            # Multiline
            actual = str(NumpyDocstring(("{}\n"
                                         "{}\n"
                                         "    this is the first line\n"
                                         "\n"
                                         "    and this is the second line\n"
                                         ).format(section, '-' * len(section))))
            expected = (".. {}::\n"
                      "\n"
                      "   this is the first line\n"
                      "   \n"
                      "   and this is the second line\n"
                      ).format(admonition)
            self.assertEqual(expected.rstrip(), actual)

            # Single line
            actual = str(NumpyDocstring(("{}\n"
                                         "{}\n"
                                         "    this is a single line\n"
                                         ).format(section, '-' * len(section))))
            expected = (".. {}:: this is a single line\n"
                      ).format(admonition)
            self.assertEqual(expected.rstrip(), actual)



    def test_parameters_with_class_reference(self):
        docstring = """\
Parameters
----------
param1 : :class:`MyClass <name.space.MyClass>` instance
"""


        actual = str(NumpyDocstring(docstring))
        expected = """\
:param param1:
:type param1: :class:`MyClass <name.space.MyClass>` instance
"""
        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring, 
            type_=SphinxNumpyDocstring)

    def test_multiple_parameters(self):
        docstring = """\
Parameters
----------
x1, x2 : array_like
    Input arrays, description of ``x1``, ``x2``.
"""


        actual = str(NumpyDocstring(dedent(docstring)))
        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`
"""
        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring, 
            type_=SphinxNumpyDocstring)

    def test_parameters_without_class_reference(self):
        docstring = """\
Parameters
----------
param1 : MyClass instance
"""


        actual = str(NumpyDocstring(dedent(docstring)))
        expected = """\
:param param1:
:type param1: MyClass instance
"""
        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring, 
            type_=SphinxNumpyDocstring)

    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
        """)
        expected = dedent("""\
            :param param1: the data to work on
            :type param1: `DataFrame`
            :param param2: a parameter with different types
            :type param2: `int` or `float` or `None`, *optional*
            :param param3: a optional mapping
            :type param3: `dict-like`, *optional*
            :param param4: a optional parameter with different types
            :type param4: `int` or `float` or `None`, *optional*
            :param param5: a optional parameter with fixed values
            :type param5: ``{"F", "C", "N"}``, *optional*
            :param param6: different default format
            :type param6: `int`, *default* `None`
            :param param7: a optional mapping
            :type param7: `mapping` of `hashable` to `str`, *optional*
            :param param8: ellipsis
            :type param8: `...` or `Ellipsis`
        """)
 
        actual = str(NumpyDocstring(docstring))
        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring,
            type_=SphinxNumpyDocstring)
    
    def test_see_also_refs_invalid(self):
        docstring = """\
See Also
--------
$funcs
123
"""
        expected = """\
.. seealso::

   $funcs
   123
"""

        self.assertEqual(expected.rstrip(), str(NumpyDocstring(docstring)))
        self.assertAlmostEqualSphinxDocstring(expected, docstring, 
            type_=SphinxNumpyDocstring)

    def test_see_also_refs(self):
        docstring = """\
numpy.multivariate_normal(mean, cov, shape=None, spam=None)

See Also
--------
some, other, funcs
otherfunc : relationship
"""

        docstring2 = """\
numpy.multivariate_normal(mean, cov, shape=None, spam=None)

See Also
--------

some, other, :func:`funcs`

otherfunc : relationship
"""

        expected = """\
numpy.multivariate_normal(mean, cov, shape=None, spam=None)

.. seealso::

   `some`, `other`, `funcs`
   
   `otherfunc`
       relationship
"""
        self.assertEqual(expected.rstrip(), str(NumpyDocstring(docstring)))
        self.assertAlmostEqualSphinxDocstring(expected, docstring, 
            type_=SphinxNumpyDocstring)

        self.assertEqual(expected.rstrip(), str(NumpyDocstring(docstring2)))
        self.assertAlmostEqualSphinxDocstring(expected, docstring2, 
            type_=SphinxNumpyDocstring)

        docstring = """\
numpy.multivariate_normal(mean, cov, shape=None, spam=None)

See Also
--------
some, other, funcs
otherfunc : relationship
"""


        expected = """\
numpy.multivariate_normal(mean, cov, shape=None, spam=None)

.. seealso::

   `some`, `other`, `funcs`
   
   `otherfunc`
       relationship
"""
        self.assertEqual(expected.rstrip(), str(NumpyDocstring(docstring)))
        self.assertAlmostEqualSphinxDocstring(expected, docstring, 
            type_=SphinxNumpyDocstring)

        docstring = """\
numpy.multivariate_normal(mean, cov, shape=None, spam=None)

See Also
--------
some, other, :func:`funcs`
otherfunc : relationship
"""

        expected = """\
numpy.multivariate_normal(mean, cov, shape=None, spam=None)

.. seealso::

   `some`, `other`, `funcs`
   
   `otherfunc`
       relationship
"""
        self.assertEqual(expected.rstrip(), str(NumpyDocstring(docstring)))
        self.assertAlmostEqualSphinxDocstring(expected, docstring, 
            type_=SphinxNumpyDocstring)

    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`
:returntype: :py:class:`~my_mod.my_class`
"""


        actual = str(NumpyDocstring(docstring))

        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring, 
            type_=SphinxNumpyDocstring)

    def test_underscore_in_attribute(self):
        docstring = """
Attributes
----------
arg_ : type
    some description
"""

        expected = """
:ivar arg_: some description
:type arg_: `type`
"""

        actual = str(NumpyDocstring(docstring))
        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring, 
            type_=SphinxNumpyDocstring)

    def test_return_types(self):
        docstring = dedent("""
            Returns
            -------
            pandas.DataFrame
                a dataframe
        """)
        expected = dedent("""
           :returns: a dataframe
           :returntype: `pandas.DataFrame`
        """)
        
        actual = str(NumpyDocstring(docstring))
        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring, 
            type_=SphinxNumpyDocstring)

    def test_yield_types(self):
        docstring = dedent("""
            Example Function

            Yields
            ------
            scalar or array-like
                The result of the computation
        """)
        expected = dedent("""
            Example Function

            :yields: The result of the computation
            :yieldtype: `scalar` or `array-like`
        """)

        actual = str(NumpyDocstring(docstring))
        self.assertEqual(expected.rstrip(), 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 CustomError: If the dimensions couldn't be parsed.
"""),
                      ################################
                      ("""
Example Function

Raises
------
AnotherError
    If the dimensions couldn't be parsed.
""", """
Example Function

:raises 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:

            actual = str(NumpyDocstring(docstring))
            self.assertEqual(expected.rstrip(), actual)
            self.assertAlmostEqualSphinxDocstring(expected, docstring, 
                type_=SphinxNumpyDocstring)

    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
:returntype: :class:`numpy.ndarray`
"""

        actual = str(NumpyDocstring(docstring))
        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring, 
            type_=SphinxNumpyDocstring)

    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

.. admonition:: Example

   Multiline example
   body
"""),
                      ################################
                      ("""
Summary line

Example
-------
Multiline example
body
""", """
Summary line

.. admonition:: Example

   Multiline example
   body
"""),
                      ################################
                      ("""
Summary line

Example
------------
Multiline example
body
""", """
Summary line

.. admonition:: Example

   Multiline example
   body
""")]
        for docstring, expected in docstrings:
            actual = str(NumpyDocstring(docstring))
            self.assertEqual(expected.rstrip(), actual)
            self.assertAlmostEqualSphinxDocstring(expected, docstring, 
                type_=SphinxNumpyDocstring)

    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`
"""

        actual = str(NumpyDocstring(docstring))
        self.assertEqual(expected.rstrip(), actual)
        self.assertAlmostEqualSphinxDocstring(expected, docstring, 
            type_=SphinxNumpyDocstring)



    def test_docstring_token_type_invalid_warnings_with_linenum(self):

        docstring = """
Description of the function. 

Args
----
param1: {1,2
param2: }
param3: 'abc
param4: def'

Returns
-------
list of int
"""


        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\):",
        )
        
        numpy_docstring = NumpyDocstring(docstring)
        numpy_warnings = numpy_docstring.warnings
        self.assertEqual(len(numpy_warnings), 4, numpy_warnings)
        for i, error in enumerate(errors):
            warn = numpy_warnings.pop(0)
            match_re = re.compile(error)
            self.assertTrue(bool(match_re.match(warn[0])), f"{error} \n do not match \n {warn[0]}")
            self.assertEqual(i+6, warn[1], msg=f"msg={warn[0]}, docstring='{str(numpy_docstring)}'") 
            # FIXME: The offset should be 5 actually, no big deal and it looks like an really painful issue to 
            # fix due to the fact that the changes in the docstring line numbers are happening at the level of napoleon.
    
    # name, expected
    escape_kwargs_tests_cases = [("x, y, z", "x, y, z"),
            ("*args, **kwargs", r"\*args, \*\*kwargs"),
            ("*x, **y", r"\*x, \*\*y") ]

    def test_escape_args_and_kwargs(self):
        
        for name, expected in self.escape_kwargs_tests_cases:

            numpy_docstring = NumpyDocstring("")
            actual = numpy_docstring._escape_args_and_kwargs(name)

            assert actual == expected

    # test docstrings for the free form text in the return secion. 
    # this feature is always enabled
    # see https://github.com/sphinx-doc/sphinx/issues/7077
    docstrings_returns = [(
        """
Single line summary

Return
------
the string of your life: `a complicated string`
the strings of your life: list of `complicated string` or str, default: ["you", "me"]
the str of your life: {"foo", "bob", "bar"}
the int of your life: int
the tuple of your life: tuple
""",

"""
Single line summary

:returns: * **the string of your life**: `a complicated string`
          * **the strings of your life**: `list` of `complicated string` or `str`, *default*: [``"you"``, ``"me"``]
          * **the str of your life**: ``{"foo", "bob", "bar"}``
          * **the int of your life**: `int`
          * **the tuple of your life**: `tuple`
        """
    ),
    
    ("""
Summary line.

Returns
-------
list of strings
    Sequence of arguments, in the order in
    which they should be called.
""",
"""
Summary line.

:returns: Sequence of arguments, in the order in
          which they should be called.
:returntype: `list` of `strings`
        """),
        
        ("""
Summary line.

Returns
-------
Sequence of arguments, in the order in
which they should be called.
""", 
"""
Summary line.

:returns: Sequence of arguments, in the order in
          which they should be called.
        """), 
        ("""
Summary line.

Returns
-------
str
""", 
"""
Summary line.

:returntype: `str`
        """),(
"""
Summary line.

Returns
-------
str
    A URL string
""",
"""
Summary line.

:returns: A URL string
:returntype: `str`
        """
        ), (
        """
Summary line.

Returns
-------
a string, can you believe it?
""",
"""
Summary line.

:returns: a string, can you believe it?
        """
        ),(
        """
Single line summary

Return
------
the string of your life
""",
"""
Single line summary

:returns: the string of your life
"""
    ) ,(
        """
Summary line.

Returns
-------
a string, can you believe it?

Raises
--
UserError
        """,
        """
Summary line.

:returns: a string, can you believe it?

:raises UserError: 
        """
        ),(
        """
Summary line.

Returns
-------
str

Raises
--
UserError

Warns
---
RuntimeWarning
        """,
        """
Summary line.

:returntype: `str`

:raises UserError:

:warns: RuntimeWarning
        """
        ),(
        """
Summary line.

Returns
-------
str
    Description of return value

Raises
--
UserError
    Description of raised exception

Warns
--------
RuntimeWarning
    Description of raised warnings
        """,
        """
Summary line.

:returns: Description of return value
:returntype: `str`

:raises UserError: Description of raised exception

:warns RuntimeWarning: Description of raised warnings
        """
        ), (
        """
Summary line.

Returns
-------
list(str)
    The lines of the docstring in a list.
    Note
    ----
    Nested markup works.
        """,
        r"""
Summary line.

:returns: The lines of the docstring in a list.

          .. note:: Nested markup works.
:returntype: `list`\ (`str`)
        """
        ), (
        """
Summary line.

Returns
-------
List[str]
    The lines of the docstring in a list.
    Note
    ----
    Nested markup works.
        """,
        r"""
Summary line.

:returns: The lines of the docstring in a list.

          .. note:: Nested markup works.
:returntype: `List`\ [`str`]
        """
        ), (
        """
Summary line.

Methods
-------
__str__()
    The lines of the docstring in a list.
    Note
    ----
    Nested markup works.
        """,
        """
Summary line.

.. admonition:: Methods

   `__str__`()
       The lines of the docstring in a list.
       
       .. note:: Nested markup works.
       """
        ), (
"""
Single line summary

Return
------
a complicated string
    Extended
    description of return value
int 
    Extended
    description of return value
the tuple of your life: tuple
    Extended
    description of return value
""",
"""
Single line summary

:returns: * a complicated string - Extended
            description of return value
          * `int` - Extended
            description of return value
          * **the tuple of your life**: `tuple` - Extended
            description of return value
"""
    ),] 
    
    # https://github.com/sphinx-contrib/napoleon/issues/12
    # https://github.com/sphinx-doc/sphinx/issues/7077
    def test_return_no_type_sphinx_issue_7077(self):
        
        for docstring, expected in self.docstrings_returns:
            
            actual = str(NumpyDocstring(docstring))
            self.assertEqual(expected.rstrip(), actual)
        
    def test_return_type_annotation_style(self):
        docstring = dedent("""
        Summary line.

        Returns
        -------
        List[Union[str, bytes, typing.Pattern]]
        """)

        expected = dedent(r"""
        Summary line.

        :returntype: `List`\ [`Union`\ [`str`, `bytes`, `typing.Pattern`]]
        """)
        actual = str(NumpyDocstring(docstring, ))
        self.assertEqual(expected.rstrip(), actual)

    def test_issue_with_link_end_of_section(self):
        # section breaks needs two white spaces with numpy-style docstrings, 
        # even if footnotes are following-up 
        
        docstring = """`PEP 484`_ type annotations are supported.

Returns
-------
bool
    True if successful, False otherwise.


.. _PEP 484:
    https://www.python.org/dev/peps/pep-0484/
        """
        expected = """`PEP 484`_ type annotations are supported.

:returns: True if successful, False otherwise.
:returntype: `bool`

.. _PEP 484:
    https://www.python.org/dev/peps/pep-0484/
        """
        actual = str(NumpyDocstring(docstring, ))
        self.assertEqual(expected.rstrip(), actual, str(actual))

        self.assertAlmostEqualSphinxDocstring(expected, docstring, 
            type_=SphinxNumpyDocstring)

        # test that Sphinx also cannot parse correctly the docstring
        # without two blank lines before new section
        # if no section header is provided
        bogus = """`PEP 484`_ type annotations are supported.

Returns
-------
bool
    True if successful, False otherwise.

.. _PEP 484:
    https://www.python.org/dev/peps/pep-0484/
"""

        expected_bogus = """`PEP 484`_ type annotations are supported.

:returns: * `bool` - True if successful, False otherwise.
          * .. _PEP 484 - https://www.python.org/dev/peps/pep-0484/
        """

        actual = str(NumpyDocstring(bogus, ))
        self.assertEqual(expected_bogus.rstrip(), actual, str(actual))
        
        # test that we have the same interpretation with sphinx
        self.assertAlmostEqualSphinxDocstring(str(NumpyDocstring(bogus, )), bogus,
            type_=SphinxNumpyDocstring)

    def test_return_type_list_free_style_do_desc(self):
        docstring = dedent("""
        Return
        ------
        the list of your life: list of str
        the str of your life: {"foo", "bob", "bar"}
        the int of your life: int
        the tuple of your life: tuple
        """)

        expected = dedent("""
        :returns: * **the list of your life**: `list` of `str`
                  * **the str of your life**: ``{"foo", "bob", "bar"}``
                  * **the int of your life**: `int`
                  * **the tuple of your life**: `tuple`
        """)

        actual = str(NumpyDocstring(docstring))
        self.assertEqual(expected.rstrip(), actual)

        docstring = dedent("""
        Yields
        ------
        the list of your life: list of str
        the str of your life: {"foo", "bob", "bar"}
        the int of your life: int
        the tuple of your life: tuple
        """)

        expected = dedent("""
        :yields: * **the list of your life**: `list` of `str`
                 * **the str of your life**: ``{"foo", "bob", "bar"}``
                 * **the int of your life**: `int`
                 * **the tuple of your life**: `tuple`
        """)

        actual = str(NumpyDocstring(docstring))
        self.assertEqual(expected.rstrip(), actual)

    def test_fields_blank_lines(self):
        """
        Test for issue https://github.com/twisted/pydoctor/issues/366
        """
        docstring = dedent("""
            Made my day
            Parameters
            ----------
            foo: str
                a string
            bob: list of str
            Returns
            -------
            bool: 
                The lines of the docstring in a list.
            Note
            ----
            Markup works.
            

            It is strong
            Yields
            ------
            tuple(ice, cream)
                Yes""")
        
        expected = dedent(r"""
        Made my day

        :param foo: a string
        :type foo: `str`
        :param bob:
        :type bob: `list` of `str`

        :returns: The lines of the docstring in a list.
        :returntype: `bool`

        .. note:: Markup works.

        It is strong

        :yields: Yes
        :yieldtype: `tuple`\ (`ice`, `cream`) 
        """)

        actual = str(NumpyDocstring(docstring))
        self.assertEqual(expected.rstrip(), actual)

    def test_fields_blank_lines_sphinx_upstream(self):
        """
        Test that sphinx napoleon upstream version of NumpyDocstring is actually generating wrong reST text (for now)...
        """
        docstring = dedent("""
            Made my day
            Parameters
            ----------
            foo: str
                a string
            bob: list of str
            Returns
            -------
            bool: 
                The lines of the docstring in a list.
            Note
            ----
            Markup works.
            

            It is strong
            Yields
            ------
            tuple(ice, cream)
                Yes""")
        
        expected_wrong = dedent(r"""
        Made my day
        :param foo: a string
        :type foo: `str`
        :param bob:
        :type bob: `list` of `str`

        :returns: The lines of the docstring in a list.
        :returntype: `bool`

        .. note:: Markup works.

        It is strong
        :Yields: `tuple`\ (`ice`, `cream`) - Yes
        """)

        self.assertAlmostEqualSphinxDocstring(expected_wrong, docstring, type_=SphinxNumpyDocstring)
        
