Skip to content

Commit

Permalink
return tuple converage
Browse files Browse the repository at this point in the history
  • Loading branch information
flying-sheep committed Jan 5, 2024
1 parent 9c7d47a commit e490cc1
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 24 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ known-first-party = ['scanpydoc']
[tool.mypy]
strict = true
explicit_package_bases = true
disallow_untyped_defs = false # handled by Ruff
mypy_path = ['$MYPY_CONFIG_FILE_DIR/src']

[tool.hatch.version]
Expand Down
18 changes: 10 additions & 8 deletions src/scanpydoc/elegant_typehints/_return_tuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@
from typing import Tuple as t_Tuple # noqa: UP035
from logging import getLogger


if sys.version_info > (3, 10):
from types import UnionType
else: # pragma: no cover
UnionType = None

from sphinx_autodoc_typehints import format_annotation


Expand All @@ -23,6 +17,14 @@
from sphinx.ext.autodoc import Options


if sys.version_info > (3, 10):
from types import UnionType

UNION_TYPES = {Union, UnionType}
else: # pragma: no cover
UNION_TYPES = {Union}


logger = getLogger(__name__)
re_ret = re.compile("^:returns?: ")

Expand Down Expand Up @@ -62,8 +64,8 @@ def process_docstring( # noqa: PLR0913
obj = inspect.unwrap(obj)
try:
hints = get_type_hints(obj)
except (AttributeError, NameError, TypeError):
# Introspecting a slot wrapper will raise TypeError
except (AttributeError, NameError, TypeError): # pragma: no cover
# Introspecting a slot wrapper can raise TypeError
return
ret_types = get_tuple_annot(hints.get("return"))
if ret_types is None:
Expand Down
94 changes: 78 additions & 16 deletions tests/test_elegant_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
get_origin,
)
from pathlib import Path
from textwrap import dedent
from collections.abc import Mapping, Callable

import pytest
Expand Down Expand Up @@ -72,8 +73,14 @@ def app(make_app_setup: Callable[..., Sphinx]) -> Sphinx:
def process_doc(app: Sphinx) -> Callable[[Callable[..., Any]], list[str]]:
def process(fn: Callable[..., Any]) -> list[str]:
lines = (inspect.getdoc(fn) or "").split("\n")
sat.process_docstring(app, "function", fn.__name__, fn, None, lines)
process_docstring(app, "function", fn.__name__, fn, None, lines)
if isinstance(fn, property):
name = fn.fget.__name__
elif hasattr(fn, "__name__"):
name = fn.__name__
else:
name = "???"
sat.process_docstring(app, "function", name, fn, None, lines)
process_docstring(app, "function", name, fn, None, lines)
return lines

return process
Expand Down Expand Up @@ -101,18 +108,54 @@ def _escape_sat(rst: str) -> str:
return f":sphinx_autodoc_typehints_type:`{rst}`"


def test_alternatives(process_doc: Callable[[Callable[..., Any]], list[str]]) -> None:
@pytest.mark.parametrize(
("kind", "add_rtype"),
[
pytest.param(lambda f: f, True, id="function"),
pytest.param(property, False, id="property"),
],
)
def test_kinds(
*,
process_doc: Callable[[Callable[..., Any]], list[str]],
kind: Callable[[Callable[..., Any]], Callable[..., Any]],
add_rtype: bool,
) -> None:
def fn_test(s: str) -> None: # pragma: no cover
""":param s: Test"""
del s

assert process_doc(fn_test) == [
assert process_doc(kind(fn_test)) == [
f":type s: {_escape_sat(':py:class:`str`')}",
":param s: Test",
NONE_RTYPE,
*([NONE_RTYPE] if add_rtype else []),
]


class CustomCls: # noqa: D101
__slots__ = ["foo"]

def meth(self): # pragma: no cover # noqa: ANN201
"""No return section and no return annotation."""


@pytest.mark.parametrize(
"obj",
[
pytest.param(None, id="none"),
pytest.param(CustomCls.foo, id="slotwrapper"), # type: ignore[attr-defined]
pytest.param(lambda: None, id="lambda"),
pytest.param(CustomCls.meth, id="func_nodoc"),
pytest.param(CustomCls().meth, id="meth_nodoc"),
],
)
def test_skip(
process_doc: Callable[[Callable[..., Any]], list[str]], obj: Callable[..., Any]
) -> None:
doc = inspect.getdoc(obj)
assert process_doc(obj) == [doc or ""]


def test_defaults_simple(
process_doc: Callable[[Callable[..., Any]], list[str]],
) -> None:
Expand Down Expand Up @@ -290,35 +333,54 @@ class B:
@pytest.mark.parametrize(
("return_ann", "foo_rendered"),
[
(tuple[str, int], ":py:class:`str`"),
(Optional[tuple[str, int]], ":py:class:`str`"),
(
pytest.param(tuple[str, int], ":py:class:`str`", id="tuple"),
pytest.param(Optional[tuple[str, int]], ":py:class:`str`", id="tuple | None"),
pytest.param(
tuple[Mapping[str, float], int],
r":py:class:`~collections.abc.Mapping`\ \["
":py:class:`str`, :py:class:`float`"
"]",
id="complex",
),
pytest.param(Optional[int], None, id="int | None"),
],
ids=["tuple", "Optional[Tuple]", "Complex"],
)
def test_return(
process_doc: Callable[[Callable[..., Any]], list[str]],
docstring: str,
return_ann: type,
foo_rendered: str,
foo_rendered: str | None,
) -> None:
def fn_test() -> None: # pragma: no cover
pass

fn_test.__doc__ = docstring
fn_test.__annotations__["return"] = return_ann
lines = [l for l in process_doc(fn_test) if not re.match("^:(rtype|param):", l)]
assert lines == [
f":return: foo : {foo_rendered}",
" A foo!",
" bar : :py:class:`int`",
" A bar!",
]
if foo_rendered is None:
assert lines == [
l
for l in dedent(docstring).strip().splitlines()
if not l.startswith(":param:")
]
else:
assert lines == [
f":return: foo : {foo_rendered}",
" A foo!",
" bar : :py:class:`int`",
" A bar!",
]


def test_return_nodoc(process_doc: Callable[[Callable[..., Any]], list[str]]) -> None:
def fn() -> tuple[int, str]: # pragma: no cover
"""No return section."""
return 1, ""

res = process_doc(fn)
assert len(res) == 3 # noqa: PLR2004
assert res[0:2] == [inspect.getdoc(fn), ""]
assert res[2].startswith(":rtype: :sphinx_autodoc_typehints_type:")


def test_load_error(make_app_setup: Callable[..., Sphinx]) -> None:
Expand Down

0 comments on commit e490cc1

Please sign in to comment.