Skip to content

Commit

Permalink
Run formatter
Browse files Browse the repository at this point in the history
Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
  • Loading branch information
gaborbernat committed Jun 15, 2023
1 parent 7f6fc35 commit 33a7dd8
Show file tree
Hide file tree
Showing 20 changed files with 249 additions and 183 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Expand Up @@ -273,7 +273,7 @@

## 1.2.1

- Fixed `` ValueError` when ``getargspec()\`\` encounters a built-in function
- Fixed ``ValueError` when``getargspec()\`\` encounters a built-in function
- Fixed `AttributeError` when `Any` is combined with another type in a `Union` (thanks Davis Kirkendall)

## 1.2.0
Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Expand Up @@ -26,8 +26,12 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Documentation :: Sphinx",
]
dynamic = [
Expand Down
89 changes: 46 additions & 43 deletions src/sphinx_autodoc_typehints/__init__.py
@@ -1,22 +1,17 @@
from __future__ import annotations

import ast
import inspect
import re
import sys
import textwrap
import types
from ast import FunctionDef, Module, stmt
from dataclasses import dataclass
from typing import Any, AnyStr, Callable, ForwardRef, NewType, TypeVar, get_type_hints
from typing import TYPE_CHECKING, Any, AnyStr, Callable, ForwardRef, NewType, TypeVar, get_type_hints

from docutils.frontend import OptionParser
from docutils.nodes import Node
from docutils.parsers.rst import Parser as RstParser
from docutils.utils import new_document
from sphinx.application import Sphinx
from sphinx.config import Config
from sphinx.environment import BuildEnvironment
from sphinx.ext.autodoc import Options
from sphinx.ext.autodoc.mock import mock
from sphinx.util import logging
from sphinx.util.inspect import signature as sphinx_signature
Expand All @@ -25,6 +20,15 @@
from .patches import install_patches
from .version import __version__

if TYPE_CHECKING:
from ast import FunctionDef, Module, stmt

from docutils.nodes import Node
from sphinx.application import Sphinx
from sphinx.config import Config
from sphinx.environment import BuildEnvironment
from sphinx.ext.autodoc import Options

_LOGGER = logging.getLogger(__name__)
_PYDATA_ANNOTATIONS = {"Any", "AnyStr", "Callable", "ClassVar", "Literal", "NoReturn", "Optional", "Tuple", "Union"}

Expand Down Expand Up @@ -59,7 +63,8 @@ def get_annotation_module(annotation: Any) -> str:
return annotation.__module__ # type: ignore # deduced Any
if hasattr(annotation, "__origin__"):
return annotation.__origin__.__module__ # type: ignore # deduced Any
raise ValueError(f"Cannot determine the module of {annotation}")
msg = f"Cannot determine the module of {annotation}"
raise ValueError(msg)


def _is_newtype(annotation: Any) -> bool:
Expand Down Expand Up @@ -130,9 +135,7 @@ def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[

def format_internal_tuple(t: tuple[Any, ...], config: Config) -> str:
# An annotation can be a tuple, e.g., for nptyping:
# NDArray[(typing.Any, ...), Float]
# In this case, format_annotation receives:
# (typing.Any, Ellipsis)
# This solution should hopefully be general for *any* type that allows tuples in annotations
fmt = [format_annotation(a, config) for a in t]
if len(fmt) == 0:
Expand All @@ -153,7 +156,7 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901 # t
# Special cases
if isinstance(annotation, ForwardRef):
return annotation.__forward_arg__
if annotation is None or annotation is type(None): # noqa: E721
if annotation is None or annotation is type(None):
return ":py:obj:`None`"
if annotation is Ellipsis:
return ":py:data:`...<Ellipsis>`"
Expand All @@ -178,10 +181,7 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901 # t
full_name = f"{module}.{class_name}" if module != "builtins" else class_name
fully_qualified: bool = getattr(config, "typehints_fully_qualified", False)
prefix = "" if fully_qualified or full_name == class_name else "~"
if module == "typing" and class_name in _PYDATA_ANNOTATIONS:
role = "data"
else:
role = "class"
role = "data" if module == "typing" and class_name in _PYDATA_ANNOTATIONS else "class"
args_format = "\\[{}]"
formatted_args: str | None = ""

Expand All @@ -200,19 +200,19 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901 # t
args_format += ")"
formatted_args = None if args else args_format
elif full_name == "typing.Optional":
args = tuple(x for x in args if x is not type(None)) # noqa: E721
args = tuple(x for x in args if x is not type(None))
elif full_name in ("typing.Union", "types.UnionType") and type(None) in args:
if len(args) == 2:
full_name = "typing.Optional"
role = "data"
args = tuple(x for x in args if x is not type(None)) # noqa: E721
args = tuple(x for x in args if x is not type(None))
else:
simplify_optional_unions: bool = getattr(config, "simplify_optional_unions", True)
if not simplify_optional_unions:
full_name = "typing.Optional"
role = "data"
args_format = f"\\[:py:data:`{prefix}typing.Union`\\[{{}}]]"
args = tuple(x for x in args if x is not type(None)) # noqa: E721
args = tuple(x for x in args if x is not type(None))
elif full_name in ("typing.Callable", "collections.abc.Callable") and args and args[0] is not ...:
fmt = [format_annotation(arg, config) for arg in args]
formatted_args = f"\\[\\[{', '.join(fmt[:-1])}], {fmt[-1]}]"
Expand All @@ -237,15 +237,12 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901 # t
# reference: https://github.com/pytorch/pytorch/pull/46548/files
def normalize_source_lines(source_lines: str) -> str:
"""
This helper function accepts a list of source lines. It finds the
indentation level of the function definition (`def`), then it indents
all lines in the function body to a point at or greater than that
level. This allows for comments and continued string literals that
are at a lower indentation than the rest of the code.
Arguments:
source_lines: source code
Returns:
source lines that have been correctly aligned
This helper function accepts a list of source lines. It finds the indentation level of the function definition
(`def`), then it indents all lines in the function body to a point at or greater than that level. This allows for
comments and continued string literals that are at a lower indentation than the rest of the code.
:param source_lines: source code
:return: source lines that have been correctly aligned
"""
lines = source_lines.split("\n")

Expand Down Expand Up @@ -280,7 +277,13 @@ def remove_prefix(text: str, prefix: str) -> str:


def process_signature(
app: Sphinx, what: str, name: str, obj: Any, options: Options, signature: str, return_annotation: str # noqa: U100
app: Sphinx,
what: str,
name: str,
obj: Any,
options: Options,
signature: str,
return_annotation: str,
) -> tuple[str, None] | None:
if not callable(obj):
return None
Expand Down Expand Up @@ -405,9 +408,6 @@ def _get_type_hint(autodoc_mock_imports: list[str], name: str, obj: Any) -> dict


def backfill_type_hints(obj: Any, name: str) -> dict[str, Any]:
parse_kwargs = {}
import ast

parse_kwargs = {"type_comments": True}

def _one_child(module: Module) -> stmt | None:
Expand Down Expand Up @@ -531,7 +531,12 @@ def format_default(app: Sphinx, default: Any, is_annotated: bool) -> str | None:


def process_docstring(
app: Sphinx, what: str, name: str, obj: Any, options: Options | None, lines: list[str] # noqa: U100
app: Sphinx,
what: str,
name: str,
obj: Any,
options: Options | None,
lines: list[str],
) -> None:
original_obj = obj
obj = obj.fget if isinstance(obj, property) else obj
Expand Down Expand Up @@ -564,16 +569,15 @@ def _get_sphinx_line_keyword_and_argument(line: str) -> tuple[str, str | None] |
>>> _get_sphinx_line_keyword_and_argument("some invalid line")
None
"""

param_line_without_description = line.split(":", maxsplit=2) # noqa: SC200
param_line_without_description = line.split(":", maxsplit=2)
if len(param_line_without_description) != 3:
return None

split_directive_and_name = param_line_without_description[1].split(maxsplit=1) # noqa: SC200
split_directive_and_name = param_line_without_description[1].split(maxsplit=1)
if len(split_directive_and_name) != 2:
if not len(split_directive_and_name):
return None
return (split_directive_and_name[0], None)
return split_directive_and_name[0], None

return tuple(split_directive_and_name) # type: ignore

Expand All @@ -591,10 +595,7 @@ def _line_is_param_line_for_arg(line: str, arg_name: str) -> bool:
if keyword not in {"param", "parameter", "arg", "argument"}:
return False

for prefix in ("", r"\*", r"\**", r"\*\*"):
if doc_name == prefix + arg_name:
return True
return False
return any(doc_name == prefix + arg_name for prefix in ("", "\\*", "\\**", "\\*\\*"))


def _inject_types_to_docstring(
Expand Down Expand Up @@ -786,14 +787,16 @@ def _inject_rtype(
lines[insert_index] = f":return: {formatted_annotation} --{line[line.find(' '):]}"


def validate_config(app: Sphinx, env: BuildEnvironment, docnames: list[str]) -> None: # noqa: U100
def validate_config(app: Sphinx, env: BuildEnvironment, docnames: list[str]) -> None:
valid = {None, "comma", "braces", "braces-after"}
if app.config.typehints_defaults not in valid | {False}:
raise ValueError(f"typehints_defaults needs to be one of {valid!r}, not {app.config.typehints_defaults!r}")
msg = f"typehints_defaults needs to be one of {valid!r}, not {app.config.typehints_defaults!r}"
raise ValueError(msg)

formatter = app.config.typehints_formatter
if formatter is not None and not callable(formatter):
raise ValueError(f"typehints_formatter needs to be callable or `None`, not {formatter}")
msg = f"typehints_formatter needs to be callable or `None`, not {formatter}"
raise ValueError(msg)


def setup(app: Sphinx) -> dict[str, bool]:
Expand Down
32 changes: 20 additions & 12 deletions src/sphinx_autodoc_typehints/attributes_patch.py
@@ -1,17 +1,22 @@
from __future__ import annotations

from functools import partial
from optparse import Values
from typing import Any, Tuple
from typing import TYPE_CHECKING, Any
from unittest.mock import patch

import sphinx.domains.python
import sphinx.ext.autodoc
from docutils.parsers.rst import Parser as RstParser
from docutils.utils import new_document
from sphinx.addnodes import desc_signature
from sphinx.application import Sphinx
from sphinx.domains.python import PyAttribute
from sphinx.ext.autodoc import AttributeDocumenter

if TYPE_CHECKING:
from optparse import Values

from sphinx.addnodes import desc_signature
from sphinx.application import Sphinx

# Defensively check for the things we want to patch
_parse_annotation = getattr(sphinx.domains.python, "_parse_annotation", None)

Expand All @@ -36,18 +41,21 @@
orig_handle_signature = PyAttribute.handle_signature


def stringify_annotation(app: Sphinx, annotation: Any, mode: str = "") -> str: # noqa: U100
"""Format the annotation with sphinx-autodoc-typehints and inject our
def stringify_annotation(app: Sphinx, annotation: Any, mode: str = "") -> str:
"""
Format the annotation with sphinx-autodoc-typehints and inject our
magic prefix to tell our patched PyAttribute.handle_signature to treat
it as rst."""
it as rst.
"""
from . import format_annotation

return TYPE_IS_RST_LABEL + format_annotation(annotation, app.config)


def patch_attribute_documenter(app: Sphinx) -> None:
"""Instead of using stringify_typehint in
`AttributeDocumenter.add_directive_header`, use `format_annotation`
"""
Instead of using stringify_typehint in
`AttributeDocumenter.add_directive_header`, use `format_annotation`.
"""

def add_directive_header(*args: Any, **kwargs: Any) -> Any:
Expand All @@ -58,7 +66,7 @@ def add_directive_header(*args: Any, **kwargs: Any) -> Any:


def rst_to_docutils(settings: Values, rst: str) -> Any:
"""Convert rst to a sequence of docutils nodes"""
"""Convert rst to a sequence of docutils nodes."""
doc = new_document("", settings)
RstParser().parse(rst, doc)
# Remove top level paragraph node so that there is no line break.
Expand All @@ -74,15 +82,15 @@ def patched_parse_annotation(settings: Values, typ: str, env: Any) -> Any:
return rst_to_docutils(settings, typ)


def patched_handle_signature(self: PyAttribute, sig: str, signode: desc_signature) -> Tuple[str, str]:
def patched_handle_signature(self: PyAttribute, sig: str, signode: desc_signature) -> tuple[str, str]:
target = "sphinx.domains.python._parse_annotation"
new_func = partial(patched_parse_annotation, self.state.document.settings)
with patch(target, new_func):
return orig_handle_signature(self, sig, signode)


def patch_attribute_handling(app: Sphinx) -> None:
"""Use format_signature to format class attribute type annotations"""
"""Use format_signature to format class attribute type annotations."""
if not OKAY_TO_PATCH:
return
PyAttribute.handle_signature = patched_handle_signature # type:ignore[method-assign]
Expand Down
28 changes: 19 additions & 9 deletions src/sphinx_autodoc_typehints/patches.py
@@ -1,16 +1,18 @@
from __future__ import annotations

from functools import lru_cache
from typing import Any
from typing import TYPE_CHECKING, Any

from docutils.parsers.rst.directives.admonitions import BaseAdmonition
from docutils.parsers.rst.states import Text
from sphinx.application import Sphinx
from sphinx.ext.autodoc import Options
from sphinx.ext.napoleon.docstring import GoogleDocstring

from .attributes_patch import patch_attribute_handling

if TYPE_CHECKING:
from sphinx.application import Sphinx
from sphinx.ext.autodoc import Options


@lru_cache # A cute way to make sure the function only runs once.
def fix_autodoc_typehints_for_overloaded_methods() -> None:
Expand All @@ -36,7 +38,12 @@ def fix_autodoc_typehints_for_overloaded_methods() -> None:


def napoleon_numpy_docstring_return_type_processor(
app: Sphinx, what: str, name: str, obj: Any, options: Options | None, lines: list[str] # noqa: U100
app: Sphinx,
what: str,
name: str,
obj: Any,
options: Options | None,
lines: list[str],
) -> None:
"""Insert a : under Returns: to tell napoleon not to look for a return type."""
if what not in ["function", "method"]:
Expand Down Expand Up @@ -73,17 +80,19 @@ def fix_napoleon_numpy_docstring_return_type(app: Sphinx) -> None:
app.connect("autodoc-process-docstring", napoleon_numpy_docstring_return_type_processor, priority=499)


def patched_lookup_annotation(*_args: Any) -> str: # noqa: U101
"""GoogleDocstring._lookup_annotation sometimes adds incorrect type
def patched_lookup_annotation(*_args: Any) -> str:
"""
GoogleDocstring._lookup_annotation sometimes adds incorrect type
annotations to constructor parameters (and otherwise does nothing). Disable
it so we can handle this on our own.
"""
return ""


def patch_google_docstring_lookup_annotation() -> None:
"""Fix issue 308:
https://github.com/tox-dev/sphinx-autodoc-typehints/issues/308
"""
Fix issue 308:
https://github.com/tox-dev/sphinx-autodoc-typehints/issues/308.
"""
GoogleDocstring._lookup_annotation = patched_lookup_annotation # type: ignore[assignment]

Expand Down Expand Up @@ -111,7 +120,8 @@ def patched_text_indent(self: Text, *args: Any) -> Any:


def patch_line_numbers() -> None:
"""Make the rst parser put line numbers on more nodes.
"""
Make the rst parser put line numbers on more nodes.
When the line numbers are missing, we have a hard time placing the :rtype:.
"""
Expand Down

0 comments on commit 33a7dd8

Please sign in to comment.