1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
|
from __future__ import annotations
import inspect
from typing import Any
from unittest.mock import MagicMock, create_autospec, patch
from sphinx.application import Sphinx
from sphinx.config import Config
import sphinx_autodoc_typehints as sat
from sphinx_autodoc_typehints import _inject_overload_signatures, process_docstring, process_signature
from sphinx_autodoc_typehints._resolver._util import get_obj_location
from sphinx_autodoc_typehints.patches import _OVERLOADS_CACHE
class _ClassWithPrivate:
def __secret(self, x: int) -> str: ...
def _make_sig_app(**overrides: Any) -> Sphinx:
defaults = {
"typehints_fully_qualified": False,
"simplify_optional_unions": False,
"typehints_formatter": None,
"typehints_use_signature": False,
"typehints_use_signature_return": False,
"autodoc_type_aliases": {},
}
defaults.update(overrides)
config = create_autospec(Config, **defaults) # ty: ignore[invalid-argument-type]
return create_autospec(Sphinx, config=config)
def _make_docstring_app(**overrides: Any) -> Sphinx:
config = MagicMock()
config.__getitem__ = getattr
config.autodoc_type_aliases = {}
config.autodoc_mock_imports = []
config.typehints_fully_qualified = False
config.simplify_optional_unions = False
config.typehints_formatter = None
config.typehints_document_rtype = True
config.typehints_document_rtype_none = True
config.typehints_use_rtype = True
config.typehints_defaults = None
config.always_document_param_types = False
config.python_display_short_literal_types = False
for key, value in overrides.items():
setattr(config, key, value)
app = MagicMock(spec=Sphinx)
app.config = config
app.env = None
return app
def test_process_signature_class_attr_lookup_fails() -> None:
"""Line 111: class name not found on module returns None."""
def fake_method(self: Any, x: int) -> str: ...
fake_method.__annotations__ = {"x": int, "return": str}
fake_method.__qualname__ = "NonExistentClass.method"
fake_method.__module__ = __name__
app = _make_sig_app()
result = process_signature(app, "method", "test.NonExistentClass.method", fake_method, MagicMock(), "", "")
assert result is None
def test_process_signature_private_name_mangling() -> None:
"""Line 114: dunder-prefixed method names get mangled."""
method = _ClassWithPrivate.__dict__["_ClassWithPrivate__secret"]
app = _make_sig_app()
result = process_signature(app, "method", "test_init._ClassWithPrivate.__secret", method, MagicMock(), "", "")
assert result is not None
sig_str, _ = result
assert "self" not in sig_str
def test_process_docstring_sphinx_signature_raises_value_error() -> None:
"""Lines 157-158: sphinx_signature raising ValueError sets signature to None."""
def func(x: int) -> str: ...
app = _make_docstring_app(typehints_document_rtype=False)
lines: list[str] = [":param x: the x"]
with patch("sphinx_autodoc_typehints.sphinx_signature", side_effect=ValueError("bad")):
process_docstring(app, "function", "func", func, None, lines)
assert ":param x: the x" in lines
def test_process_docstring_sphinx_signature_raises_type_error() -> None:
"""Lines 157-158: sphinx_signature raising TypeError sets signature to None."""
def func(x: int) -> str: ...
app = _make_docstring_app(typehints_document_rtype=False)
lines: list[str] = [":param x: the x"]
with patch("sphinx_autodoc_typehints.sphinx_signature", side_effect=TypeError("bad")):
process_docstring(app, "function", "func", func, None, lines)
assert ":param x: the x" in lines
def test_inject_overload_no_qualname() -> None:
"""Line 198: obj without __qualname__ returns False."""
obj = MagicMock(spec=[])
obj.__module__ = "some_module"
obj.__qualname__ = ""
_OVERLOADS_CACHE["some_module"] = {}
try:
app = _make_docstring_app()
assert _inject_overload_signatures(app, "function", "name", obj, []) is False
finally:
_OVERLOADS_CACHE.pop("some_module", None)
def test_inject_overload_unannotated_param_and_no_return() -> None:
"""Lines 214, 217->224: overload with unannotated param and no return annotation."""
obj = MagicMock()
obj.__module__ = "test_mod"
obj.__qualname__ = "func"
sig = inspect.Signature(
parameters=[inspect.Parameter("x", inspect.Parameter.POSITIONAL_OR_KEYWORD)],
)
_OVERLOADS_CACHE["test_mod"] = {"func": [sig]}
try:
app = _make_docstring_app()
lines: list[str] = []
result = _inject_overload_signatures(app, "function", "name", obj, lines)
assert result is True
joined = "\n".join(lines)
assert "**x**" in joined
assert "\u2192" not in joined
finally:
_OVERLOADS_CACHE.pop("test_mod", None)
def test_inject_overload_empty_overloads_returns_false() -> None:
"""Line 233: no matching overloads returns False."""
obj = MagicMock()
obj.__module__ = "test_mod2"
obj.__qualname__ = "func"
_OVERLOADS_CACHE["test_mod2"] = {"other_func": []}
try:
app = _make_docstring_app()
assert _inject_overload_signatures(app, "function", "name", obj, []) is False
finally:
_OVERLOADS_CACHE.pop("test_mod2", None)
def test_local_function_warning_includes_location() -> None:
def fake_method(self: Any, x: int) -> str: ...
fake_method.__annotations__ = {"x": int, "return": str}
fake_method.__qualname__ = "outer.<locals>.inner"
fake_method.__module__ = __name__
app = _make_sig_app()
mock_logger = MagicMock()
with patch("sphinx_autodoc_typehints._LOGGER", mock_logger):
process_signature(app, "method", "test.outer.<locals>.inner", fake_method, MagicMock(), "", "")
mock_logger.warning.assert_called_once()
kwargs = mock_logger.warning.call_args.kwargs
assert "location" in kwargs
assert kwargs["location"] == get_obj_location(fake_method)
def test_inject_types_no_signature() -> None:
"""Branch 261->263: signature is None skips _inject_signature."""
def sample() -> str: ...
app = _make_docstring_app(typehints_document_rtype=False)
lines: list[str] = []
sat._inject_types_to_docstring({"return": str}, None, sample, app, "function", "sample", lines) # noqa: SLF001
assert not any(line.startswith(":type") for line in lines)
|