diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c66c43f..73824f79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,11 @@ ## 1.21.5 -- More robust determination of rtype location / fix issue 302 +- More robust determination of rtype location / fix issue 302 ## 1.21.4 -- Improvements to the location of the return type +- Improvements to the location of the return type ## 1.21.3 diff --git a/pyproject.toml b/pyproject.toml index 1c690574..a49d5335 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ testpaths = ["tests"] [tool.mypy] python_version = "3.10" strict = true -exclude = "^.*/roots/.*$" +exclude = "^(.*/roots/.*)|(tests/test_integration.py)$" [[tool.mypy.overrides]] diff --git a/tests/roots/test-dummy/dummy_module.py b/tests/roots/test-dummy/dummy_module.py index d7921930..65d35a89 100644 --- a/tests/roots/test-dummy/dummy_module.py +++ b/tests/roots/test-dummy/dummy_module.py @@ -1,249 +1,4 @@ from dataclasses import dataclass -from mailbox import Mailbox -from types import CodeType -from typing import Callable, Optional, Union, overload - - -def get_local_function(): - def wrapper(self) -> str: - """ - Wrapper - """ - - return wrapper - - -class Class: - """ - Initializer docstring. - - :param x: foo - :param y: bar - :param z: baz - """ - - def __init__(self, x: bool, y: int, z: Optional[str] = None) -> None: # noqa: U100 - pass - - def a_method(self, x: bool, y: int, z: Optional[str] = None) -> str: # noqa: U100 - """ - Method docstring. - - :param x: foo - :param y: bar - :param z: baz - """ - - def _private_method(self, x: str) -> str: # noqa: U100 - """ - Private method docstring. - - :param x: foo - """ - - def __dunder_method(self, x: str) -> str: # noqa: U100 - """ - Dunder method docstring. - - :param x: foo - """ - - def __magic_custom_method__(self, x: str) -> str: # noqa: U100 - """ - Magic dunder method docstring. - - :param x: foo - """ - - @classmethod - def a_classmethod(cls, x: bool, y: int, z: Optional[str] = None) -> str: # noqa: U100 - """ - Classmethod docstring. - - :param x: foo - :param y: bar - :param z: baz - """ - - @staticmethod - def a_staticmethod(x: bool, y: int, z: Optional[str] = None) -> str: # noqa: U100 - """ - Staticmethod docstring. - - :param x: foo - :param y: bar - :param z: baz - """ - - @property - def a_property(self) -> str: - """ - Property docstring - """ - - class InnerClass: - """ - Inner class. - """ - - def inner_method(self, x: bool) -> str: # noqa: U100 - """ - Inner method. - - :param x: foo - """ - - def __dunder_inner_method(self, x: bool) -> str: # noqa: U100 - """ - Dunder inner method. - - :param x: foo - """ - - locally_defined_callable_field = get_local_function() - - -class DummyException(Exception): # noqa: N818 - """ - Exception docstring - - :param message: blah - """ - - def __init__(self, message: str) -> None: - super().__init__(message) - - -def function(x: bool, y: int, z_: Optional[str] = None) -> str: # noqa: U100 - """ - Function docstring. - - :param x: foo - :param y: bar - :param z\\_: baz - :return: something - :rtype: bytes - """ - - -def function_with_starred_documentation_param_names(*args: int, **kwargs: str): # noqa: U100 - r""" - Function docstring. - - Usage:: - - print(1) - - :param \*args: foo - :param \**kwargs: bar - """ - - -def function_with_escaped_default(x: str = "\b"): # noqa: U100 - """ - Function docstring. - - :param x: foo - """ - - -def function_with_unresolvable_annotation(x: "a.b.c"): # noqa: U100,F821 - """ - Function docstring. - - :arg x: foo - """ - - -def function_with_typehint_comment( - x, # type: int # noqa: U100 - y, # type: str # noqa: U100 -): - # type: (...) -> None - """ - Function docstring. - - :parameter x: foo - :parameter y: bar - """ - - -class ClassWithTypehints: - """ - Class docstring. - - :param x: foo - """ - - def __init__( - self, x # type: int # noqa: U100 - ): - # type: (...) -> None - pass - - def foo( - self, x # type: str # noqa: U100 - ): - # type: (...) -> int - """ - Method docstring. - - :arg x: foo - """ - return 42 - - def method_without_typehint(self, x): # noqa: U100 - """ - Method docstring. - """ - # test that multiline str can be correctly indented - multiline_str = """ -test -""" - return multiline_str - - -def function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs): # noqa: U100 - # type: (Union[str, bytes, None], *str, bytes, **int) -> None - """ - Function docstring. - - :arg x: foo - :argument y: bar - :parameter z: baz - :parameter kwargs: some kwargs - """ - - -class ClassWithTypehintsNotInline: - """ - Class docstring. - - :param x: foo - """ - - def __init__(self, x=None): # noqa: U100 - # type: (Optional[Callable[[int, bytes], int]]) -> None - pass - - def foo(self, x=1): - # type: (Callable[[int, bytes], int]) -> int - """ - Method docstring. - - :param x: foo - """ - return x(1, b"") - - @classmethod - def mk(cls, x=None): - # type: (Optional[Callable[[int, bytes], int]]) -> ClassWithTypehintsNotInline - """ - Method docstring. - - :param x: foo - """ - return cls(x) def undocumented_function(x: int) -> str: @@ -257,153 +12,3 @@ class DataClass: """Class docstring.""" x: int - - -class Decorator: - """ - Initializer docstring. - - :param func: function - """ - - def __init__(self, func: Callable[[int, str], str]): # noqa: U100 - pass - - -def mocked_import(x: Mailbox): # noqa: U100 - """ - A docstring. - - :param x: function - """ - - -def func_with_examples() -> int: - """ - A docstring. - - .. rubric:: Examples - - Here are a couple of examples of how to use this function. - """ - - -@overload -def func_with_overload(a: int, b: int) -> None: # noqa: U100 - ... - - -@overload -def func_with_overload(a: str, b: str) -> None: # noqa: U100 - ... - - -def func_with_overload(a: Union[int, str], b: Union[int, str]) -> None: # noqa: U100 - """ - f does the thing. The arguments can either be ints or strings but they must - both have the same type. - - Parameters - ---------- - a: - The first thing - b: - The second thing - """ - - -class TestClassAttributeDocs: - """A class""" - - code: Union[CodeType, None] - """An attribute""" - - -def func_with_examples_and_returns_after() -> int: - """ - f does the thing. - - Examples - -------- - - Here is an example - - :returns: The index of the widget - """ - - -def func_with_parameters_and_stuff_after(a: int, b: int) -> int: # noqa: U100 - """A func - - :param a: a tells us something - :param b: b tells us something - - More info about the function here. - """ - - -def func_with_rtype_in_weird_spot(a: int, b: int) -> int: # noqa: U100 - """A func - - :param a: a tells us something - :param b: b tells us something - - Examples - -------- - - Here is an example - - :returns: The index of the widget - - More info about the function here. - - :rtype: int - """ - - -def empty_line_between_parameters(a: int, b: int) -> int: # noqa: U100 - """A func - - :param a: One of the following possibilities: - - - a - - - b - - - c - - :param b: Whatever else we have to say. - - There is more of it And here too - - More stuff here. - """ - - -def func_with_code_block() -> int: - """ - A docstring. - - You would say: - - .. code-block:: - - print("some python code here") - - - .. rubric:: Examples - - Here are a couple of examples of how to use this function. - """ - - -def func_with_definition_list() -> int: - """Some text and then a definition list. - - abc - x - - xyz - something - """ - # See https://github.com/tox-dev/sphinx-autodoc-typehints/issues/302 diff --git a/tests/roots/test-dummy/index.rst b/tests/roots/test-dummy/index.rst deleted file mode 100644 index 98d967f1..00000000 --- a/tests/roots/test-dummy/index.rst +++ /dev/null @@ -1,59 +0,0 @@ -Dummy Module -============ - -.. autoclass:: dummy_module.Class - :members: - :undoc-members: - :private-members: - :special-members: __magic_custom_method__ - -.. autoexception:: dummy_module.DummyException - :members: - :undoc-members: - -.. autofunction:: dummy_module.function - -.. autofunction:: dummy_module.function_with_escaped_default - -.. autofunction:: dummy_module.function_with_unresolvable_annotation - -.. autofunction:: dummy_module.function_with_typehint_comment - -.. autofunction:: dummy_module.function_with_starred_documentation_param_names - -.. autoclass:: dummy_module.ClassWithTypehints - :members: - -.. autofunction:: dummy_module.function_with_typehint_comment_not_inline - -.. autoclass:: dummy_module.ClassWithTypehintsNotInline - :members: - -.. autofunction:: dummy_module.undocumented_function - -.. autoclass:: dummy_module.DataClass - :undoc-members: - :special-members: __init__ - -.. autodecorator:: dummy_module.Decorator - -.. autofunction:: dummy_module.mocked_import - -.. autofunction:: dummy_module.func_with_examples - -.. autofunction:: dummy_module.func_with_overload - -.. autoclass:: dummy_module.TestClassAttributeDocs - :members: - -.. autofunction:: dummy_module.func_with_examples_and_returns_after - -.. autofunction:: dummy_module.func_with_parameters_and_stuff_after - -.. autofunction:: dummy_module.func_with_rtype_in_weird_spot - -.. autofunction:: dummy_module.empty_line_between_parameters - -.. autofunction:: dummy_module.func_with_code_block - -.. autofunction:: dummy_module.func_with_definition_list diff --git a/tests/roots/test-integration/conf.py b/tests/roots/test-integration/conf.py new file mode 100644 index 00000000..14a5de11 --- /dev/null +++ b/tests/roots/test-integration/conf.py @@ -0,0 +1,14 @@ +import pathlib +import sys + +# Make dummy_module.py available for autodoc. +sys.path.insert(0, str(pathlib.Path(__file__).parent)) + + +master_doc = "index" + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx_autodoc_typehints", +] diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 00000000..08854586 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,918 @@ +import re +import sys +from dataclasses import dataclass +from inspect import isclass +from io import StringIO +from mailbox import Mailbox +from pathlib import Path +from textwrap import dedent +from types import CodeType +from typing import Any, Callable, Optional, TypeVar, Union, overload + +import pytest +from sphinx.testing.util import SphinxTestApp + +T = TypeVar("T") + + +def expected(expected: str) -> Callable[[T], T]: + def dec(val: T) -> T: + val.EXPECTED = expected + return val + + return dec + + +def warns(pattern: str) -> Callable[[T], T]: + def dec(val: T) -> T: + val.WARNING = pattern + return val + + return dec + + +@expected("mod.get_local_function()") +def get_local_function(): + def wrapper(self) -> str: + """ + Wrapper + """ + + return wrapper + + +@warns("Cannot handle as a local function") +@expected( + """\ +class mod.Class(x, y, z=None) + + Initializer docstring. + + Parameters: + * **x** ("bool") -- foo + + * **y** ("int") -- bar + + * **z** ("Optional"["str"]) -- baz + + class InnerClass + + Inner class. + + inner_method(x) + + Inner method. + + Parameters: + **x** ("bool") -- foo + + Return type: + "str" + + classmethod a_classmethod(x, y, z=None) + + Classmethod docstring. + + Parameters: + * **x** ("bool") -- foo + + * **y** ("int") -- bar + + * **z** ("Optional"["str"]) -- baz + + Return type: + "str" + + a_method(x, y, z=None) + + Method docstring. + + Parameters: + * **x** ("bool") -- foo + + * **y** ("int") -- bar + + * **z** ("Optional"["str"]) -- baz + + Return type: + "str" + + property a_property: str + + Property docstring + + static a_staticmethod(x, y, z=None) + + Staticmethod docstring. + + Parameters: + * **x** ("bool") -- foo + + * **y** ("int") -- bar + + * **z** ("Optional"["str"]) -- baz + + Return type: + "str" + + locally_defined_callable_field() -> str + + Wrapper + + Return type: + "str" + """ +) +class Class: + """ + Initializer docstring. + + :param x: foo + :param y: bar + :param z: baz + """ + + def __init__(self, x: bool, y: int, z: Optional[str] = None) -> None: # noqa: U100 + pass + + def a_method(self, x: bool, y: int, z: Optional[str] = None) -> str: # noqa: U100 + """ + Method docstring. + + :param x: foo + :param y: bar + :param z: baz + """ + + def _private_method(self, x: str) -> str: # noqa: U100 + """ + Private method docstring. + + :param x: foo + """ + + def __dunder_method(self, x: str) -> str: # noqa: U100 + """ + Dunder method docstring. + + :param x: foo + """ + + def __magic_custom_method__(self, x: str) -> str: # noqa: U100 + """ + Magic dunder method docstring. + + :param x: foo + """ + + @classmethod + def a_classmethod(cls, x: bool, y: int, z: Optional[str] = None) -> str: # noqa: U100 + """ + Classmethod docstring. + + :param x: foo + :param y: bar + :param z: baz + """ + + @staticmethod + def a_staticmethod(x: bool, y: int, z: Optional[str] = None) -> str: # noqa: U100 + """ + Staticmethod docstring. + + :param x: foo + :param y: bar + :param z: baz + """ + + @property + def a_property(self) -> str: + """ + Property docstring + """ + + class InnerClass: + """ + Inner class. + """ + + def inner_method(self, x: bool) -> str: # noqa: U100 + """ + Inner method. + + :param x: foo + """ + + def __dunder_inner_method(self, x: bool) -> str: # noqa: U100 + """ + Dunder inner method. + + :param x: foo + """ + + locally_defined_callable_field = get_local_function() + + +@expected( + """\ +exception mod.DummyException(message) + + Exception docstring + + Parameters: + **message** ("str") -- blah +""" +) +class DummyException(Exception): # noqa: N818 + """ + Exception docstring + + :param message: blah + """ + + def __init__(self, message: str) -> None: + super().__init__(message) + + +@expected( + """\ +mod.function(x, y, z_=None) + + Function docstring. + + Parameters: + * **x** ("bool") -- foo + + * **y** ("int") -- bar + + * **z_** ("Optional"["str"]) -- baz + + Returns: + something + + Return type: + bytes +""" +) +def function(x: bool, y: int, z_: Optional[str] = None) -> str: # noqa: U100 + """ + Function docstring. + + :param x: foo + :param y: bar + :param z\\_: baz + :return: something + :rtype: bytes + """ + + +@expected( + """\ +mod.function_with_starred_documentation_param_names(*args, **kwargs) + + Function docstring. + + Usage: + + print(1) + + Parameters: + * ***args** ("int") -- foo + + * ****kwargs** ("str") -- bar +""" +) +def function_with_starred_documentation_param_names(*args: int, **kwargs: str): # noqa: U100 + r""" + Function docstring. + + Usage:: + + print(1) + + :param \*args: foo + :param \**kwargs: bar + """ + + +@expected( + """\ +mod.function_with_escaped_default(x='\\\\x08') + + Function docstring. + + Parameters: + **x** ("str") -- foo +""" +) +def function_with_escaped_default(x: str = "\b"): # noqa: U100 + """ + Function docstring. + + :param x: foo + """ + + +@warns("Cannot resolve forward reference in type annotations") +@expected( + """\ +mod.function_with_unresolvable_annotation(x) + + Function docstring. + + Parameters: + **x** (*a.b.c*) -- foo +""" +) +def function_with_unresolvable_annotation(x: "a.b.c"): # noqa: U100,F821 + """ + Function docstring. + + :arg x: foo + """ + + +@expected( + """\ +mod.function_with_typehint_comment(x, y) + + Function docstring. + + Parameters: + * **x** ("int") -- foo + + * **y** ("str") -- bar + + Return type: + "None" +""" +) +def function_with_typehint_comment( + x, # type: int # noqa: U100 + y, # type: str # noqa: U100 +): + # type: (...) -> None + """ + Function docstring. + + :parameter x: foo + :parameter y: bar + """ + + +@expected( + """\ +class mod.ClassWithTypehints(x) + + Class docstring. + + Parameters: + **x** ("int") -- foo + + foo(x) + + Method docstring. + + Parameters: + **x** ("str") -- foo + + Return type: + "int" + + method_without_typehint(x) + + Method docstring. +""" +) +class ClassWithTypehints: + """ + Class docstring. + + :param x: foo + """ + + def __init__( + self, x # type: int # noqa: U100 + ): + # type: (...) -> None + pass + + def foo( + self, x # type: str # noqa: U100 + ): + # type: (...) -> int + """ + Method docstring. + + :arg x: foo + """ + return 42 + + def method_without_typehint(self, x): # noqa: U100 + """ + Method docstring. + """ + # test that multiline str can be correctly indented + multiline_str = """ +test +""" + return multiline_str + + +@expected( + """\ +mod.function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs) + + Function docstring. + + Parameters: + * **x** ("Union"["str", "bytes", "None"]) -- foo + + * **y** ("str") -- bar + + * **z** ("bytes") -- baz + + * **kwargs** ("int") -- some kwargs + + Return type: + "None" +""" +) +def function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs): # noqa: U100 + # type: (Union[str, bytes, None], *str, bytes, **int) -> None + """ + Function docstring. + + :arg x: foo + :argument y: bar + :parameter z: baz + :parameter kwargs: some kwargs + """ + + +@expected( + """\ +class mod.ClassWithTypehintsNotInline(x=None) + + Class docstring. + + Parameters: + **x** ("Optional"["Callable"[["int", "bytes"], "int"]]) -- foo + + foo(x=1) + + Method docstring. + + Parameters: + **x** ("Callable"[["int", "bytes"], "int"]) -- foo + + Return type: + "int" + + classmethod mk(x=None) + + Method docstring. + + Parameters: + **x** ("Optional"["Callable"[["int", "bytes"], "int"]]) -- + foo + + Return type: + "ClassWithTypehintsNotInline" +""" +) +class ClassWithTypehintsNotInline: + """ + Class docstring. + + :param x: foo + """ + + def __init__(self, x=None): # noqa: U100 + # type: (Optional[Callable[[int, bytes], int]]) -> None + pass + + def foo(self, x=1): + # type: (Callable[[int, bytes], int]) -> int + """ + Method docstring. + + :param x: foo + """ + return x(1, b"") + + @classmethod + def mk(cls, x=None): + # type: (Optional[Callable[[int, bytes], int]]) -> ClassWithTypehintsNotInline + """ + Method docstring. + + :param x: foo + """ + return cls(x) + + +@expected( + """\ +mod.undocumented_function(x) + + Hi + + Return type: + "str" +""" +) +def undocumented_function(x: int) -> str: + """Hi""" + + return str(x) + + +@expected( + """\ +class mod.DataClass(x) + + Class docstring. +""" +) +@dataclass +class DataClass: + """Class docstring.""" + + x: int + + +@expected( + """\ +class mod.Decorator(func) + + Initializer docstring. + + Parameters: + **func** ("Callable"[["int", "str"], "str"]) -- function +""" +) +class Decorator: + """ + Initializer docstring. + + :param func: function + """ + + def __init__(self, func: Callable[[int, str], str]): # noqa: U100 + pass + + +@expected( + """\ +mod.mocked_import(x) + + A docstring. + + Parameters: + **x** ("Mailbox") -- function +""" +) +def mocked_import(x: Mailbox): # noqa: U100 + """ + A docstring. + + :param x: function + """ + + +@expected( + """\ +mod.func_with_examples() + + A docstring. + + Return type: + "int" + + -[ Examples ]- + + Here are a couple of examples of how to use this function. +""" +) +def func_with_examples() -> int: + """ + A docstring. + + .. rubric:: Examples + + Here are a couple of examples of how to use this function. + """ + + +@overload +def func_with_overload(a: int, b: int) -> None: # noqa: U100 + ... + + +@overload +def func_with_overload(a: str, b: str) -> None: # noqa: U100 + ... + + +@expected( + """\ +mod.func_with_overload(a, b) + + f does the thing. The arguments can either be ints or strings but + they must both have the same type. + + Parameters: + * **a** ("Union"["int", "str"]) -- The first thing + + * **b** ("Union"["int", "str"]) -- The second thing + + Return type: + "None" +""" +) +def func_with_overload(a: Union[int, str], b: Union[int, str]) -> None: # noqa: U100 + """ + f does the thing. The arguments can either be ints or strings but they must + both have the same type. + + Parameters + ---------- + a: + The first thing + b: + The second thing + """ + + +@expected( + """\ +class mod.TestClassAttributeDocs + + A class + + code: "Optional"["CodeType"] + + An attribute +""" +) +class TestClassAttributeDocs: + """A class""" + + code: Union[CodeType, None] + """An attribute""" + + +@expected( + """\ +mod.func_with_examples_and_returns_after() + + f does the thing. + + -[ Examples ]- + + Here is an example + + Return type: + "int" + + Returns: + The index of the widget +""" +) +def func_with_examples_and_returns_after() -> int: + """ + f does the thing. + + Examples + -------- + + Here is an example + + :returns: The index of the widget + """ + + +@expected( + """\ +mod.func_with_parameters_and_stuff_after(a, b) + + A func + + Parameters: + * **a** ("int") -- a tells us something + + * **b** ("int") -- b tells us something + + Return type: + "int" + + More info about the function here. +""" +) +def func_with_parameters_and_stuff_after(a: int, b: int) -> int: # noqa: U100 + """A func + + :param a: a tells us something + :param b: b tells us something + + More info about the function here. + """ + + +@expected( + """\ +mod.func_with_rtype_in_weird_spot(a, b) + + A func + + Parameters: + * **a** ("int") -- a tells us something + + * **b** ("int") -- b tells us something + + -[ Examples ]- + + Here is an example + + Returns: + The index of the widget + + More info about the function here. + + Return type: + int +""" +) +def func_with_rtype_in_weird_spot(a: int, b: int) -> int: # noqa: U100 + """A func + + :param a: a tells us something + :param b: b tells us something + + Examples + -------- + + Here is an example + + :returns: The index of the widget + + More info about the function here. + + :rtype: int + """ + + +@expected( + """\ +mod.empty_line_between_parameters(a, b) + + A func + + Parameters: + * **a** ("int") -- + + One of the following possibilities: + + * a + + * b + + * c + + * **b** ("int") -- + + Whatever else we have to say. + + There is more of it And here too + + Return type: + "int" + + More stuff here. +""" +) +def empty_line_between_parameters(a: int, b: int) -> int: # noqa: U100 + """A func + + :param a: One of the following possibilities: + + - a + + - b + + - c + + :param b: Whatever else we have to say. + + There is more of it And here too + + More stuff here. + """ + + +@expected( + """\ +mod.func_with_code_block() + + A docstring. + + You would say: + + print("some python code here") + + Return type: + "int" + + -[ Examples ]- + + Here are a couple of examples of how to use this function. +""" +) +def func_with_code_block() -> int: + """ + A docstring. + + You would say: + + .. code-block:: + + print("some python code here") + + + .. rubric:: Examples + + Here are a couple of examples of how to use this function. + """ + + +@expected( + """ + mod.func_with_definition_list() + + Some text and then a definition list. + + Return type: + "int" + + abc + x + + xyz + something + """ +) +def func_with_definition_list() -> int: + """Some text and then a definition list. + + abc + x + + xyz + something + """ + # See https://github.com/tox-dev/sphinx-autodoc-typehints/issues/302 + + +AUTO_FUNCTION = ".. autofunction:: mod.{}" +AUTO_CLASS = """\ +.. autoclass:: mod.{} + :members: +""" +AUTO_EXCEPTION = """\ +.. autoexception:: mod.{} + :members: +""" + + +@pytest.mark.parametrize("object", [x for x in globals().values() if hasattr(x, "EXPECTED")]) +@pytest.mark.sphinx("text", testroot="integration") +def test_integration(app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch, object: Any) -> None: + if isclass(object) and issubclass(object, BaseException): + template = AUTO_EXCEPTION + elif isclass(object): + template = AUTO_CLASS + else: + template = AUTO_FUNCTION + + (Path(app.srcdir) / "index.rst").write_text(template.format(object.__name__)) + monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__]) + app.build() + assert "build succeeded" in status.getvalue() # Build succeeded + + regexp = getattr(object, "WARNING", None) + value = warning.getvalue().strip() + if regexp: + msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}" + assert re.search(regexp, value), msg + else: + assert not value + + result = (Path(app.srcdir) / "_build/text/index.txt").read_text() + + expected = object.EXPECTED + try: + assert result.strip() == dedent(expected).strip() + except Exception: + print("Result was:\n", result, "\n\n") + raise diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index 36bc4093..a25a6e6c 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -1,13 +1,13 @@ from __future__ import annotations import collections.abc -import pathlib import re import sys import types import typing from functools import cmp_to_key from io import StringIO +from pathlib import Path from textwrap import dedent, indent from types import FunctionType, ModuleType from typing import ( @@ -429,49 +429,42 @@ def test_process_docstring_slot_wrapper() -> None: def set_python_path() -> None: - test_path = pathlib.Path(__file__).parent + test_path = Path(__file__).parent # Add test directory to sys.path to allow imports of dummy module. if str(test_path) not in sys.path: sys.path.insert(0, str(test_path)) -def maybe_fix_py310(expected_contents: str) -> str: - if PY310_PLUS: - for old, new in [ - ("*bool** | **None*", '"Optional"["bool"]'), - ("*int** | **str** | **float*", '"int" | "str" | "float"'), - ("*str** | **None*", '"Optional"["str"]'), - ("(*bool*)", '("bool")'), - ("(*int*", '("int"'), - (" str", ' "str"'), - ('"Optional"["str"]', '"Optional"["str"]'), - ('"Optional"["Callable"[["int", "bytes"], "int"]]', '"Optional"["Callable"[["int", "bytes"], "int"]]'), - ]: - expected_contents = expected_contents.replace(old, new) - return expected_contents - - @pytest.mark.parametrize("always_document_param_types", [True, False], ids=["doc_param_type", "no_doc_param_type"]) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) -def test_sphinx_output( +def test_always_document_param_types( app: SphinxTestApp, status: StringIO, warning: StringIO, always_document_param_types: bool ) -> None: set_python_path() app.config.always_document_param_types = always_document_param_types # type: ignore # create flag app.config.autodoc_mock_imports = ["mailbox"] # type: ignore # create flag - if sys.version_info < (3, 7): - app.config.autodoc_mock_imports.append("dummy_module_future_annotations") + + # Prevent "document isn't included in any toctree" warnings + for f in Path(app.srcdir).glob("*.rst"): + f.unlink() + (Path(app.srcdir) / "index.rst").write_text( + dedent( + """ + .. autofunction:: dummy_module.undocumented_function + + .. autoclass:: dummy_module.DataClass + :undoc-members: + :special-members: __init__ + """ + ) + ) + app.build() assert "build succeeded" in status.getvalue() # Build succeeded - - # There should be a warning about an unresolved forward reference - warnings = warning.getvalue().strip() - assert "Cannot resolve forward reference in type annotations of " in warnings, warnings - # There should not be warnings about incorrect block endings. - assert "Field list ends without a blank line; unexpected unindent." not in warnings, warnings + assert not warning.getvalue().strip() format_args = {} for indentation_level in range(2): @@ -481,421 +474,40 @@ def test_sphinx_output( else: format_args[key] = "" - text_path = pathlib.Path(app.srcdir) / "_build" / "text" / "index.txt" - with text_path.open("r") as f: - text_contents = f.read().replace("–", "--") - expected_contents = """\ - Dummy Module - ************ - - class dummy_module.Class(x, y, z=None) - - Initializer docstring. - - Parameters: - * **x** ("bool") – foo - - * **y** ("int") – bar - - * **z** ("Optional"["str"]) – baz - - class InnerClass - - Inner class. - - __dunder_inner_method(x) - - Dunder inner method. - - Parameters: - **x** ("bool") -- foo - - Return type: - "str" - - inner_method(x) - - Inner method. - - Parameters: - **x** ("bool") -- foo - - Return type: - "str" - - __dunder_method(x) - - Dunder method docstring. - - Parameters: - **x** ("str") -- foo - - Return type: - "str" - - __magic_custom_method__(x) - - Magic dunder method docstring. - - Parameters: - **x** ("str") -- foo - - Return type: - "str" - - _private_method(x) - - Private method docstring. - - Parameters: - **x** ("str") -- foo - - Return type: - "str" - - classmethod a_classmethod(x, y, z=None) - - Classmethod docstring. - - Parameters: - * **x** ("bool") – foo - - * **y** ("int") – bar - - * **z** ("Optional"["str"]) – baz - - Return type: - "str" - - a_method(x, y, z=None) - - Method docstring. - - Parameters: - * **x** ("bool") – foo - - * **y** ("int") – bar - - * **z** ("Optional"["str"]) – baz - - Return type: - "str" - - property a_property: str - - Property docstring - - static a_staticmethod(x, y, z=None) - - Staticmethod docstring. - - Parameters: - * **x** ("bool") – foo - - * **y** ("int") – bar - - * **z** ("Optional"["str"]) – baz - - Return type: - "str" - - locally_defined_callable_field() -> str - - Wrapper - - Return type: - "str" - - exception dummy_module.DummyException(message) - - Exception docstring - - Parameters: - **message** ("str") – blah - - dummy_module.function(x, y, z_=None) - - Function docstring. - - Parameters: - * **x** ("bool") – foo - - * **y** ("int") – bar - - * **z_** ("Optional"["str"]) – baz - - Returns: - something - - Return type: - bytes - - dummy_module.function_with_escaped_default(x='\\\\x08') - - Function docstring. - - Parameters: - **x** ("str") – foo - - dummy_module.function_with_unresolvable_annotation(x) - - Function docstring. - - Parameters: - **x** (*a.b.c*) – foo - - dummy_module.function_with_typehint_comment(x, y) - - Function docstring. - - Parameters: - * **x** ("int") – foo - - * **y** ("str") – bar - - Return type: - "None" - - dummy_module.function_with_starred_documentation_param_names(*args, **kwargs) - - Function docstring. - - Usage: - - print(1) - - Parameters: - * ***args** ("int") -- foo - - * ****kwargs** ("str") -- bar - - class dummy_module.ClassWithTypehints(x) - - Class docstring. - - Parameters: - **x** ("int") -- foo - - foo(x) - - Method docstring. - - Parameters: - **x** ("str") -- foo - - Return type: - "int" - - method_without_typehint(x) - - Method docstring. - - dummy_module.function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs) - - Function docstring. - - Parameters: - * **x** ("Union"["str", "bytes", "None"]) -- foo - - * **y** ("str") -- bar - - * **z** ("bytes") -- baz - - * **kwargs** ("int") -- some kwargs - - Return type: - "None" - - class dummy_module.ClassWithTypehintsNotInline(x=None) - - Class docstring. - - Parameters: - **x** ("Optional"["Callable"[["int", "bytes"], "int"]]) -- foo - - foo(x=1) - - Method docstring. - - Parameters: - **x** ("Callable"[["int", "bytes"], "int"]) -- foo - - Return type: - "int" - - classmethod mk(x=None) - - Method docstring. - - Parameters: - **x** ("Optional"["Callable"[["int", "bytes"], "int"]]) -- foo - - Return type: - "ClassWithTypehintsNotInline" - - dummy_module.undocumented_function(x) - - Hi{undoc_params_0} - - Return type: - "str" - - class dummy_module.DataClass(x) - - Class docstring.{undoc_params_0} - - __init__(x){undoc_params_1} - - @dummy_module.Decorator(func) - - Initializer docstring. - - Parameters: - **func** ("Callable"[["int", "str"], "str"]) -- function - - dummy_module.mocked_import(x) - - A docstring. - - Parameters: - **x** ("Mailbox") -- function - - dummy_module.func_with_examples() - - A docstring. - - Return type: - "int" - - -[ Examples ]- - - Here are a couple of examples of how to use this function. - - dummy_module.func_with_overload(a, b) - - f does the thing. The arguments can either be ints or strings but they must both have the same type. - - Parameters: - * **a** ("Union"["int", "str"]) -- The first thing - - * **b** ("Union"["int", "str"]) -- The second thing - - Return type: - "None" - - class dummy_module.TestClassAttributeDocs - - A class - - code: "Optional"["CodeType"] - - An attribute - - dummy_module.func_with_examples_and_returns_after() - - f does the thing. - - -[ Examples ]- - - Here is an example - - Return type: - "int" - - Returns: - The index of the widget - - dummy_module.func_with_parameters_and_stuff_after(a, b) - - A func - - Parameters: - * **a** ("int") -- a tells us something - - * **b** ("int") -- b tells us something - - Return type: - "int" - - More info about the function here. - - dummy_module.func_with_rtype_in_weird_spot(a, b) - - A func - - Parameters: - * **a** ("int") -- a tells us something - - * **b** ("int") -- b tells us something - - -[ Examples ]- - - Here is an example - - Returns: - The index of the widget - - More info about the function here. - - Return type: - int - - dummy_module.empty_line_between_parameters(a, b) - - A func - - Parameters: - * **a** ("int") -- - - One of the following possibilities: - - * a - - * b - - * c - - * **b** ("int") -- - - Whatever else we have to say. - - There is more of it And here too - - Return type: - "int" - - More stuff here. - - dummy_module.func_with_code_block() - - A docstring. - - You would say: - - print("some python code here") - - Return type: - "int" + contents = (Path(app.srcdir) / "_build/text/index.txt").read_text() + expected_contents = """\ + dummy_module.undocumented_function(x) - -[ Examples ]- + Hi{undoc_params_0} - Here are a couple of examples of how to use this function. + Return type: + "str" - dummy_module.func_with_definition_list() + class dummy_module.DataClass(x) - Some text and then a definition list. + Class docstring.{undoc_params_0} - Return type: - "int" + __init__(x){undoc_params_1} + """ + expected_contents = dedent(expected_contents).format(**format_args) + assert contents == expected_contents - abc - x - xyz - something - """ - expected_contents = dedent(expected_contents).format(**format_args).replace("–", "--") - assert text_contents == maybe_fix_py310(expected_contents) +def maybe_fix_py310(expected_contents: str) -> str: + if not PY310_PLUS: + return expected_contents + for old, new in [ + ("*bool** | **None*", '"Optional"["bool"]'), + ("*int** | **str** | **float*", '"int" | "str" | "float"'), + ("*str** | **None*", '"Optional"["str"]'), + ("(*bool*)", '("bool")'), + ("(*int*", '("int"'), + (" str", ' "str"'), + ('"Optional"["str"]', '"Optional"["str"]'), + ('"Optional"["Callable"[["int", "bytes"], "int"]]', '"Optional"["Callable"[["int", "bytes"], "int"]]'), + ]: + expected_contents = expected_contents.replace(old, new) + return expected_contents @pytest.mark.sphinx("text", testroot="dummy") @@ -908,29 +520,27 @@ def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) assert "build succeeded" in status.getvalue() # Build succeeded - text_path = pathlib.Path(app.srcdir) / "_build" / "text" / "future_annotations.txt" - with text_path.open("r") as f: - text_contents = f.read().replace("–", "--") - expected_contents = """\ - Dummy Module - ************ + contents = (Path(app.srcdir) / "_build/text/future_annotations.txt").read_text() + expected_contents = """\ + Dummy Module + ************ - dummy_module_future_annotations.function_with_py310_annotations(self, x, y, z=None) + dummy_module_future_annotations.function_with_py310_annotations(self, x, y, z=None) - Method docstring. + Method docstring. - Parameters: - * **x** (*bool** | **None*) -- foo + Parameters: + * **x** (*bool** | **None*) -- foo - * **y** (*int** | **str** | **float*) -- bar + * **y** (*int** | **str** | **float*) -- bar - * **z** (*str** | **None*) -- baz + * **z** (*str** | **None*) -- baz - Return type: - str - """ - expected_contents = maybe_fix_py310(dedent(expected_contents)) - assert text_contents == expected_contents + Return type: + str + """ + expected_contents = maybe_fix_py310(dedent(expected_contents)) + assert contents == expected_contents @pytest.mark.parametrize( @@ -961,8 +571,7 @@ def test_sphinx_output_defaults( return assert "build succeeded" in status.getvalue() - text_path = pathlib.Path(app.srcdir) / "_build" / "text" / "simple.txt" - text_contents = text_path.read_text().replace("–", "--") + contents = (Path(app.srcdir) / "_build/text/simple.txt").read_text() expected_contents = f"""\ Simple Module ************* @@ -979,7 +588,7 @@ def test_sphinx_output_defaults( Return type: "str" """ - assert text_contents == dedent(expected_contents) + assert contents == dedent(expected_contents) @pytest.mark.parametrize( @@ -1009,8 +618,7 @@ def test_sphinx_output_formatter( assert not isinstance(expected, Exception), "Expected app.build() to raise exception, but it didn’t" assert "build succeeded" in status.getvalue() - text_path = pathlib.Path(app.srcdir) / "_build" / "text" / "simple.txt" - text_contents = text_path.read_text().replace("–", "--") + contents = (Path(app.srcdir) / "_build/text/simple.txt").read_text() expected_contents = f"""\ Simple Module ************* @@ -1027,7 +635,7 @@ def test_sphinx_output_formatter( Return type: {expected[2]} """ - assert text_contents == dedent(expected_contents) + assert contents == dedent(expected_contents) def test_normalize_source_lines_async_def() -> None: @@ -1149,7 +757,7 @@ def test_sphinx_output_formatter_no_use_rtype(app: SphinxTestApp, status: String app.config.typehints_use_rtype = False # type: ignore app.build() assert "build succeeded" in status.getvalue() - text_path = pathlib.Path(app.srcdir) / "_build" / "text" / "simple_no_use_rtype.txt" + text_path = Path(app.srcdir) / "_build" / "text" / "simple_no_use_rtype.txt" text_contents = text_path.read_text().replace("–", "--") expected_contents = """\ Simple Module @@ -1214,7 +822,7 @@ def test_sphinx_output_with_use_signature(app: SphinxTestApp, status: StringIO) app.config.typehints_use_signature = True # type: ignore app.build() assert "build succeeded" in status.getvalue() - text_path = pathlib.Path(app.srcdir) / "_build" / "text" / "simple.txt" + text_path = Path(app.srcdir) / "_build" / "text" / "simple.txt" text_contents = text_path.read_text().replace("–", "--") expected_contents = """\ Simple Module @@ -1243,7 +851,7 @@ def test_sphinx_output_with_use_signature_return(app: SphinxTestApp, status: Str app.config.typehints_use_signature_return = True # type: ignore app.build() assert "build succeeded" in status.getvalue() - text_path = pathlib.Path(app.srcdir) / "_build" / "text" / "simple.txt" + text_path = Path(app.srcdir) / "_build" / "text" / "simple.txt" text_contents = text_path.read_text().replace("–", "--") expected_contents = """\ Simple Module @@ -1273,7 +881,7 @@ def test_sphinx_output_with_use_signature_and_return(app: SphinxTestApp, status: app.config.typehints_use_signature_return = True # type: ignore app.build() assert "build succeeded" in status.getvalue() - text_path = pathlib.Path(app.srcdir) / "_build" / "text" / "simple.txt" + text_path = Path(app.srcdir) / "_build" / "text" / "simple.txt" text_contents = text_path.read_text().replace("–", "--") expected_contents = """\ Simple Module @@ -1302,7 +910,7 @@ def test_default_annotation_without_typehints(app: SphinxTestApp, status: String app.config.typehints_defaults = "comma" # type: ignore app.build() assert "build succeeded" in status.getvalue() - text_path = pathlib.Path(app.srcdir) / "_build" / "text" / "without_complete_typehints.txt" + text_path = Path(app.srcdir) / "_build" / "text" / "without_complete_typehints.txt" text_contents = text_path.read_text().replace("–", "--") expected_contents = """\ Simple Module diff --git a/whitelist.txt b/whitelist.txt index 61f7890a..0ef2f946 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -57,6 +57,7 @@ rootdir rst rtype runtime +setitem sig signode skipif @@ -71,10 +72,12 @@ supertype tempdir testroot textwrap +toctree typ typehint typehints unittest +unlink unresolvable util utils