diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9a29fd75..763b5500 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,76 +2,33 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - - id: check-ast - - id: check-builtin-literals - - id: check-docstring-first - - id: check-merge-conflict - - id: check-yaml - - id: check-toml - - id: debug-statements - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.0.272" hooks: - - id: pyupgrade - args: ["--py38-plus"] - exclude: "^(tests/roots/test-dummy/dummy_module.py)$" - - id: pyupgrade - files: "^(tests/roots/test-dummy/dummy_module.py)$" - args: ["--py36-plus"] - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort + - id: ruff + args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black - args: [--safe] - - repo: https://github.com/asottile/blacken-docs - rev: 1.13.0 - hooks: - - id: blacken-docs - additional_dependencies: [black==23.3] - - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.10.0 - hooks: - - id: rst-backticks - repo: https://github.com/tox-dev/tox-ini-fmt rev: "1.3.0" hooks: - id: tox-ini-fmt args: ["-p", "fix"] - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + - repo: https://github.com/tox-dev/pyproject-fmt + rev: "0.11.2" hooks: - - id: flake8 - additional_dependencies: - - flake8-bugbear==23.3.23 - - flake8-comprehensions==3.12 - - flake8-pytest-style==1.7.2 - - flake8-spellcheck==0.28 - - flake8-unused-arguments==0.0.13 - - flake8-noqa==1.3.1 - - pep8-naming==0.13.3 + - id: pyproject-fmt + additional_dependencies: ["tox>=4.6"] - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v2.7.1" + rev: "v3.0.0-alpha.9-for-vscode" hooks: - id: prettier - additional_dependencies: - - prettier@2.7.1 - - "@prettier/plugin-xml@2.2" args: ["--print-width=120", "--prose-wrap=always"] - - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.33.0 - hooks: - - id: markdownlint - repo: meta hooks: - id: check-hooks-apply - id: check-useless-excludes - - repo: https://github.com/tox-dev/pyproject-fmt - rev: "0.9.2" - hooks: - - id: pyproject-fmt diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ffac0b2..c17fb4cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/pyproject.toml b/pyproject.toml index babb2a36..62a96ce2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.3", - "hatchling>=1.14", + "hatchling>=1.17.1", ] [project] @@ -26,30 +26,34 @@ 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 = [ "version", ] dependencies = [ - "Sphinx>=6.1.3", + "Sphinx>=7.0.1", ] optional-dependencies.docs = [ - "furo>=2023.3.27", - "sphinx>=6.1.3", + "furo>=2023.5.20", + "sphinx>=7.0.1", "sphinx-autodoc-typehints>=1.23.4", ] optional-dependencies.testing = [ "covdefaults>=2.3", - "coverage>=7.2.3", + "coverage>=7.2.7", "diff-cover>=7.5", "nptyping>=2.5", "pytest>=7.3.1", - "pytest-cov>=4", + "pytest-cov>=4.1", "sphobjinv>=2.3.1", - "typing-extensions>=4.5", + "typing-extensions>=4.6.3", ] optional-dependencies.type-comment = [ 'typed-ast>=1.5.4; python_version < "3.8"', @@ -66,11 +70,6 @@ version.source = "vcs" [tool.black] line-length = 120 -[tool.isort] -line_length = 120 -profile = "black" -known_first_party = ["sphinx_autodoc_typehints", "tests"] - [tool.pytest.ini_options] testpaths = ["tests"] @@ -94,8 +93,26 @@ run.plugins = ["covdefaults"] python_version = "3.10" strict = true exclude = "^(.*/roots/.*)|(tests/test_integration.py)$" +overrides = [{ module = ["sphobjinv.*"], ignore_missing_imports = true }] - -[[tool.mypy.overrides]] -module = ["sphobjinv"] -ignore_missing_imports = true +[tool.ruff] +select = ["ALL"] +line-length = 120 +target-version = "py37" +isort = {known-first-party = ["tox", "tests"], required-imports = ["from __future__ import annotations"]} +ignore = [ + "ANN101", # no typoe annotation for self + "ANN401", # allow Any as type annotation + "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible + "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible + "S104", # Possible binding to all interface +] +[tool.ruff.per-file-ignores] +"tests/**/*.py" = [ + "S101", # asserts allowed in tests... + "FBT", # don"t care about booleans as positional arguments in tests + "INP001", # no implicit namespace + "D", # don"t care about documentation in tests + "S603", # `subprocess` call: check for execution of untrusted input + "PLR2004", # Magic value used in comparison, consider replacing with a constant variable +] diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py index c1bd24a7..f6e1beaf 100644 --- a/src/sphinx_autodoc_typehints/__init__.py +++ b/src/sphinx_autodoc_typehints/__init__.py @@ -1,22 +1,18 @@ +"""Sphinx autodoc type hints.""" 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 @@ -25,6 +21,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"} @@ -38,12 +43,18 @@ def _get_types_type(obj: Any) -> str | None: try: return _TYPES_DICT.get(obj) - except Exception: + except Exception: # noqa: BLE001 # e.g. exception: unhashable type return None def get_annotation_module(annotation: Any) -> str: + """ + Get module for an annotation. + + :param annotation: + :return: + """ if annotation is None: return "builtins" if _get_types_type(annotation) is not None: @@ -56,20 +67,27 @@ def get_annotation_module(annotation: Any) -> str: ): return "typing" if hasattr(annotation, "__module__"): - return annotation.__module__ # type: ignore # deduced Any + return annotation.__module__ # type: ignore[no-any-return] if hasattr(annotation, "__origin__"): - return annotation.__origin__.__module__ # type: ignore # deduced Any - raise ValueError(f"Cannot determine the module of {annotation}") + return annotation.__origin__.__module__ # type: ignore[no-any-return] + msg = f"Cannot determine the module of {annotation}" + raise ValueError(msg) def _is_newtype(annotation: Any) -> bool: if sys.version_info < (3, 10): return inspect.isfunction(annotation) and hasattr(annotation, "__supertype__") - else: - return isinstance(annotation, NewType) + return isinstance(annotation, NewType) + +def get_annotation_class_name(annotation: Any, module: str) -> str: # noqa: C901, PLR0911 + """ + Get class name for annotation. -def get_annotation_class_name(annotation: Any, module: str) -> str: + :param annotation: + :param module: + :return: + """ # Special cases if annotation is None: return "None" @@ -82,25 +100,33 @@ def get_annotation_class_name(annotation: Any, module: str) -> str: return "NewType" if getattr(annotation, "__qualname__", None): - return annotation.__qualname__ # type: ignore # deduced Any - elif getattr(annotation, "_name", None): # Required for generic aliases on Python 3.7+ - return annotation._name # type: ignore # deduced Any - elif module in ("typing", "typing_extensions") and isinstance(getattr(annotation, "name", None), str): + return annotation.__qualname__ # type: ignore[no-any-return] + if getattr(annotation, "_name", None): # Required for generic aliases on Python 3.7+ + return annotation._name # type: ignore[no-any-return] # noqa: SLF001 + if module in ("typing", "typing_extensions") and isinstance(getattr(annotation, "name", None), str): # Required for at least Pattern and Match - return annotation.name # type: ignore # deduced Any + return annotation.name # type: ignore[no-any-return] origin = getattr(annotation, "__origin__", None) if origin: if getattr(origin, "__qualname__", None): # Required for Protocol subclasses - return origin.__qualname__ # type: ignore # deduced Any - elif getattr(origin, "_name", None): # Required for Union on Python 3.7+ - return origin._name # type: ignore # deduced Any + return origin.__qualname__ # type: ignore[no-any-return] + if getattr(origin, "_name", None): # Required for Union on Python 3.7+ + return origin._name # type: ignore[no-any-return] # noqa: SLF001 annotation_cls = annotation if inspect.isclass(annotation) else type(annotation) return annotation_cls.__qualname__.lstrip("_") -def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[Any, ...]: +def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[Any, ...]: # noqa: PLR0911 + """ + Get annotation arguments. + + :param annotation: + :param module: + :param class_name: + :return: + """ try: original = getattr(sys.modules[module], class_name) except (KeyError, AttributeError): @@ -112,38 +138,42 @@ def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[ # Special cases if class_name in ("Pattern", "Match") and hasattr(annotation, "type_var"): # Python < 3.7 return (annotation.type_var,) - elif class_name == "ClassVar" and hasattr(annotation, "__type__"): # ClassVar on Python < 3.7 + if class_name == "ClassVar" and hasattr(annotation, "__type__"): # ClassVar on Python < 3.7 return (annotation.__type__,) - elif class_name == "TypeVar" and hasattr(annotation, "__constraints__"): - return annotation.__constraints__ # type: ignore # no stubs defined - elif class_name == "NewType" and hasattr(annotation, "__supertype__"): + if class_name == "TypeVar" and hasattr(annotation, "__constraints__"): + return annotation.__constraints__ # type: ignore[no-any-return] + if class_name == "NewType" and hasattr(annotation, "__supertype__"): return (annotation.__supertype__,) - elif class_name == "Literal" and hasattr(annotation, "__values__"): - return annotation.__values__ # type: ignore # deduced Any - elif class_name == "Generic": - return annotation.__parameters__ # type: ignore # deduced Any + if class_name == "Literal" and hasattr(annotation, "__values__"): + return annotation.__values__ # type: ignore[no-any-return] + if class_name == "Generic": + return annotation.__parameters__ # type: ignore[no-any-return] result = getattr(annotation, "__args__", ()) # 3.10 and earlier Tuple[()] returns ((), ) instead of () the tuple does - result = () if len(result) == 1 and result[0] == () else result # type: ignore + result = () if len(result) == 1 and result[0] == () else result # type: ignore[misc] return result 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: return "()" - elif len(fmt) == 1: + if len(fmt) == 1: return f"({fmt[0]}, )" - else: - return f"({', '.join(fmt)})" + return f"({', '.join(fmt)})" -def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901 # too complex +def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PLR0911, PLR0912, PLR0915 + """ + Format the annotation. + + :param annotation: + :param config: + :return: + """ typehints_formatter: Callable[..., str] | None = getattr(config, "typehints_formatter", None) if typehints_formatter is not None: formatted = typehints_formatter(annotation, config) @@ -153,7 +183,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:`...`" @@ -178,10 +208,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 = "" @@ -200,19 +227,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: + if len(args) == 2: # noqa: PLR2004 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]}]" @@ -230,22 +257,20 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901 # t fmt = [format_annotation(arg, config) for arg in args] formatted_args = args_format.format(", ".join(fmt)) - result = f":py:{role}:`{prefix}{full_name}`{formatted_args}" - return result + return f":py:{role}:`{prefix}{full_name}`{formatted_args}" # 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 + Normalize the 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") @@ -253,13 +278,13 @@ def remove_prefix(text: str, prefix: str) -> str: return text[text.startswith(prefix) and len(prefix) :] # Find the line and line number containing the function definition - for i, l in enumerate(lines): - if l.lstrip().startswith("def "): - idx = i + for pos, line in enumerate(lines): + if line.lstrip().startswith("def "): + idx = pos whitespace_separator = "def" break - elif l.lstrip().startswith("async def"): - idx = i + if line.lstrip().startswith("async def"): + idx = pos whitespace_separator = "async def" break @@ -279,9 +304,27 @@ def remove_prefix(text: str, prefix: str) -> str: return "\n".join(aligned_prefix + aligned_suffix) -def process_signature( - app: Sphinx, what: str, name: str, obj: Any, options: Options, signature: str, return_annotation: str # noqa: U100 +def process_signature( # noqa: C901, PLR0913 + app: Sphinx, + what: str, + name: str, + obj: Any, + options: Options, # noqa: ARG001 + signature: str, # noqa: ARG001 + return_annotation: str, # noqa: ARG001 ) -> tuple[str, None] | None: + """ + Process the signature. + + :param app: + :param what: + :param name: + :param obj: + :param options: + :param signature: + :param return_annotation: + :return: + """ if not callable(obj): return None @@ -382,8 +425,8 @@ def _resolve_type_guarded_imports(autodoc_mock_imports: list[str], obj: Any) -> guarded_code = textwrap.dedent(part) try: with mock(autodoc_mock_imports): - exec(guarded_code, obj.__globals__) - except Exception as exc: + exec(guarded_code, obj.__globals__) # noqa: S102 + except Exception as exc: # noqa: BLE001 _LOGGER.warning(f"Failed guarded type import with {exc!r}") @@ -404,10 +447,14 @@ def _get_type_hint(autodoc_mock_imports: list[str], name: str, obj: Any) -> dict return result -def backfill_type_hints(obj: Any, name: str) -> dict[str, Any]: - parse_kwargs = {} - import ast +def backfill_type_hints(obj: Any, name: str) -> dict[str, Any]: # noqa: C901, PLR0911, PLR0912 + """ + Backfill type hints. + :param obj: the object + :param name: the name + :return: backfilled value + """ parse_kwargs = {"type_comments": True} def _one_child(module: Module) -> stmt | None: @@ -419,7 +466,7 @@ def _one_child(module: Module) -> stmt | None: try: code = textwrap.dedent(normalize_source_lines(inspect.getsource(obj))) - obj_ast = ast.parse(code, **parse_kwargs) # type: ignore # dynamic kwargs + obj_ast = ast.parse(code, **parse_kwargs) # type: ignore[call-overload] # dynamic kwargs except (OSError, TypeError, SyntaxError): return {} @@ -461,10 +508,7 @@ def _one_child(module: Module) -> stmt | None: if arg_key is None: continue - if is_inline: # the type information now is tied to the argument - value = getattr(arg, "type_comment", None) - else: # type data from comment - value = comment_args[at] + value = getattr(arg, "type_comment", None) if is_inline else comment_args[at] if value is not None: rv[arg_key] = value @@ -513,7 +557,7 @@ def add(val: str) -> None: return result -def format_default(app: Sphinx, default: Any, is_annotated: bool) -> str | None: +def format_default(app: Sphinx, default: Any, is_annotated: bool) -> str | None: # noqa: FBT001 if default is inspect.Parameter.empty: return None formatted = repr(default).replace("\\", "\\\\") @@ -521,18 +565,31 @@ def format_default(app: Sphinx, default: Any, is_annotated: bool) -> str | None: if is_annotated: if app.config.typehints_defaults.startswith("braces"): return f" (default: ``{formatted}``)" - else: # other option is comma - return f", default: ``{formatted}``" - else: - if app.config.typehints_defaults == "braces-after": - return f" (default: ``{formatted}``)" - else: - return f"default: ``{formatted}``" + return f", default: ``{formatted}``" + if app.config.typehints_defaults == "braces-after": + return f" (default: ``{formatted}``)" + return f"default: ``{formatted}``" -def process_docstring( - app: Sphinx, what: str, name: str, obj: Any, options: Options | None, lines: list[str] # noqa: U100 +def process_docstring( # noqa: PLR0913 + app: Sphinx, + what: str, + name: str, + obj: Any, + options: Options | None, # noqa: ARG001 + lines: list[str], ) -> None: + """ + Process the docstring for an entry. + + :param app: the Sphinx app + :param what: the target + :param name: the name + :param obj: the object + :param options: the options + :param lines: the lines + :return: + """ original_obj = obj obj = obj.fget if isinstance(obj, property) else obj if not callable(obj): @@ -545,7 +602,7 @@ def process_docstring( except (ValueError, TypeError): signature = None type_hints = get_all_type_hints(app.config.autodoc_mock_imports, obj, name) - app.config._annotation_globals = getattr(obj, "__globals__", {}) # type: ignore # config has no such attribute + app.config._annotation_globals = getattr(obj, "__globals__", {}) # type: ignore[attr-defined] # noqa: SLF001 try: _inject_types_to_docstring(type_hints, signature, original_obj, app, what, name, lines) finally: @@ -564,18 +621,17 @@ 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 - if len(param_line_without_description) != 3: + param_line_without_description = line.split(":", maxsplit=2) + if len(param_line_without_description) != 3: # noqa: PLR2004 return None - split_directive_and_name = param_line_without_description[1].split(maxsplit=1) # noqa: SC200 - if len(split_directive_and_name) != 2: + split_directive_and_name = param_line_without_description[1].split(maxsplit=1) + if len(split_directive_and_name) != 2: # noqa: PLR2004 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 + return tuple(split_directive_and_name) # type: ignore[return-value] def _line_is_param_line_for_arg(line: str, arg_name: str) -> bool: @@ -591,13 +647,10 @@ 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( +def _inject_types_to_docstring( # noqa: PLR0913 type_hints: dict[str, Any], signature: inspect.Signature | None, original_obj: Any, @@ -612,7 +665,7 @@ def _inject_types_to_docstring( _inject_rtype(type_hints, original_obj, app, what, name, lines) -def _inject_signature( +def _inject_signature( # noqa: C901 type_hints: dict[str, Any], signature: inspect.Signature, app: Sphinx, @@ -624,14 +677,15 @@ def _inject_signature( default = signature.parameters[arg_name].default if arg_name.endswith("_"): - arg_name = f"{arg_name[:-1]}\\_" + arg_name = f"{arg_name[:-1]}\\_" # noqa: PLW2901 insert_index = None for at, line in enumerate(lines): if _line_is_param_line_for_arg(line, arg_name): # Get the arg_name from the doc to match up for type in case it has a star prefix. # Line is in the correct format so this is guaranteed to return tuple[str, str]. - _, arg_name = _get_sphinx_line_keyword_and_argument(line) # type: ignore[assignment, misc] + func = _get_sphinx_line_keyword_and_argument + _, arg_name = func(line) # type: ignore[assignment, misc] # noqa: PLW2901 insert_index = at break @@ -672,17 +726,13 @@ class InsertIndexInfo: def node_line_no(node: Node) -> int | None: """ - Get the 1-indexed line on which the node starts if possible. If not, return - None. + Get the 1-indexed line on which the node starts if possible. If not, return None. - Descend through the first children until we locate one with a line number or - return None if None of them have one. + Descend through the first children until we locate one with a line number or return None if None of them have one. - I'm not aware of any rst on which this returns None, to find out would - require a more detailed analysis of the docutils rst parser source code. An - example where the node doesn't have a line number but the first child does - is all `definition_list` nodes. It seems like bullet_list and option_list - get line numbers, but enum_list also doesn't. *shrug*. + I'm not aware of any rst on which this returns None, to find out would require a more detailed analysis of the + docutils rst parser source code. An example where the node doesn't have a line number but the first child does is + all `definition_list` nodes. It seems like bullet_list and option_list get line numbers, but enum_list also doesn't. """ while node.line is None and node.children: node = node.children[0] @@ -731,7 +781,6 @@ def get_insert_index(app: Sphinx, lines: list[str]) -> InsertIndexInfo | None: return InsertIndexInfo(insert_index=at, found_param=True) # 4. Insert before examples - # TODO: Maybe adjust which tags to insert ahead of for child in doc.children: if tag_name(child) in ["literal_block", "paragraph", "field_list"]: continue @@ -743,7 +792,7 @@ def get_insert_index(app: Sphinx, lines: list[str]) -> InsertIndexInfo | None: return InsertIndexInfo(insert_index=len(lines)) -def _inject_rtype( +def _inject_rtype( # noqa: PLR0913 type_hints: dict[str, Any], original_obj: Any, app: Sphinx, @@ -769,7 +818,7 @@ def _inject_rtype( formatted_annotation = format_annotation(type_hints["return"], app.config) - if r.found_param and insert_index < len(lines) and lines[insert_index].strip() != "": + if r.found_param and insert_index < len(lines) and lines[insert_index].strip(): insert_index -= 1 if insert_index == len(lines) and not r.found_param: @@ -786,26 +835,28 @@ 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: # noqa: ARG001 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]: - app.add_config_value("always_document_param_types", False, "html") - app.add_config_value("typehints_fully_qualified", False, "env") - app.add_config_value("typehints_document_rtype", True, "env") - app.add_config_value("typehints_use_rtype", True, "env") + app.add_config_value("always_document_param_types", False, "html") # noqa: FBT003 + app.add_config_value("typehints_fully_qualified", False, "env") # noqa: FBT003 + app.add_config_value("typehints_document_rtype", True, "env") # noqa: FBT003 + app.add_config_value("typehints_use_rtype", True, "env") # noqa: FBT003 app.add_config_value("typehints_defaults", None, "env") - app.add_config_value("simplify_optional_unions", True, "env") + app.add_config_value("simplify_optional_unions", True, "env") # noqa: FBT003 app.add_config_value("typehints_formatter", None, "env") - app.add_config_value("typehints_use_signature", False, "env") - app.add_config_value("typehints_use_signature_return", False, "env") + app.add_config_value("typehints_use_signature", False, "env") # noqa: FBT003 + app.add_config_value("typehints_use_signature_return", False, "env") # noqa: FBT003 app.connect("env-before-read-docs", validate_config) # config may be changed after “config-inited” event app.connect("autodoc-process-signature", process_signature) app.connect("autodoc-process-docstring", process_docstring) diff --git a/src/sphinx_autodoc_typehints/attributes_patch.py b/src/sphinx_autodoc_typehints/attributes_patch.py index b5c571fb..807fab3e 100644 --- a/src/sphinx_autodoc_typehints/attributes_patch.py +++ b/src/sphinx_autodoc_typehints/attributes_patch.py @@ -1,17 +1,23 @@ +"""Patch for attributes.""" +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) @@ -36,29 +42,26 @@ 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 - magic prefix to tell our patched PyAttribute.handle_signature to treat - it as rst.""" +def _stringify_annotation(app: Sphinx, annotation: Any, mode: str = "") -> str: # noqa: ARG001 + # Format the annotation with sphinx-autodoc-typehints and inject our magic prefix to tell our patched + # PyAttribute.handle_signature to treat 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: - with patch(STRINGIFY_PATCH_TARGET, partial(stringify_annotation, app)): + with patch(STRINGIFY_PATCH_TARGET, partial(_stringify_annotation, app)): return orig_add_directive_header(*args, **kwargs) AttributeDocumenter.add_directive_header = add_directive_header # type:ignore[method-assign] 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. @@ -68,13 +71,14 @@ def rst_to_docutils(settings: Values, rst: str) -> Any: def patched_parse_annotation(settings: Values, typ: str, env: Any) -> Any: # if typ doesn't start with our label, use original function if not typ.startswith(TYPE_IS_RST_LABEL): - return _parse_annotation(typ, env) # type: ignore + assert _parse_annotation is not None # noqa: S101 + return _parse_annotation(typ, env) # Otherwise handle as rst typ = typ[len(TYPE_IS_RST_LABEL) :] 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): @@ -82,7 +86,7 @@ def patched_handle_signature(self: PyAttribute, sig: str, signode: desc_signatur 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] diff --git a/src/sphinx_autodoc_typehints/patches.py b/src/sphinx_autodoc_typehints/patches.py index 193528c1..0ee0cc34 100644 --- a/src/sphinx_autodoc_typehints/patches.py +++ b/src/sphinx_autodoc_typehints/patches.py @@ -1,31 +1,29 @@ +"""Custom patches to make the world work.""" 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: """ - sphinx-autodoc-typehints responds to the "autodoc-process-signature" event - to remove types from the signature line of functions. - - Normally, `FunctionDocumenter.format_signature` and - `MethodDocumenter.format_signature` call `super().format_signature` which - ends up going to `Documenter.format_signature`, and this last method emits - the `autodoc-process-signature` event. However, if there are overloads, - `FunctionDocumenter.format_signature` does something else and the event - never occurs. + sphinx-autodoc-typehints responds to the "autodoc-process-signature" event to remove types from the signature line. - Here we remove this alternative code path by brute force. + Normally, `FunctionDocumenter.format_signature` and `MethodDocumenter.format_signature` call + `super().format_signature` which ends up going to `Documenter.format_signature`, and this last method emits the + `autodoc-process-signature` event. However, if there are overloads, `FunctionDocumenter.format_signature` does + something else and the event never occurs. Here we remove this alternative code path by brute force. See https://github.com/tox-dev/sphinx-autodoc-typehints/issues/296 """ @@ -35,8 +33,13 @@ def fix_autodoc_typehints_for_overloaded_methods() -> None: del MethodDocumenter.format_signature -def napoleon_numpy_docstring_return_type_processor( - app: Sphinx, what: str, name: str, obj: Any, options: Options | None, lines: list[str] # noqa: U100 +def napoleon_numpy_docstring_return_type_processor( # noqa: PLR0913 + app: Sphinx, + what: str, + name: str, # noqa: ARG001 + obj: Any, # noqa: ARG001 + options: Options | None, # noqa: ARG001 + lines: list[str], ) -> None: """Insert a : under Returns: to tell napoleon not to look for a return type.""" if what not in ["function", "method"]: @@ -47,51 +50,47 @@ def napoleon_numpy_docstring_return_type_processor( # Search for the returns header: # Returns: # -------- - for idx, line in enumerate(lines[:-2]): + for pos, line in enumerate(lines[:-2]): if line.lower().strip(":") not in ["return", "returns"]: continue # Underline detection. - chars = set(lines[idx + 1].strip()) + chars = set(lines[pos + 1].strip()) # Napoleon allows the underline to consist of a bunch of weirder things... if len(chars) != 1 or list(chars)[0] not in "=-~_*+#": continue - idx = idx + 2 + pos = pos + 2 # noqa: PLW2901 break else: return - lines.insert(idx, ":") + lines.insert(pos, ":") def fix_napoleon_numpy_docstring_return_type(app: Sphinx) -> None: - """ - If no return type is explicitly provided, numpy docstrings will mess up and - use the return type text as return types. - """ + """If no return type is explicitly set, numpy docstrings will use the return type text as return types.""" # standard priority is 500. Setting priority to 499 ensures this runs before # napoleon's docstring processor. 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 - annotations to constructor parameters (and otherwise does nothing). Disable - it so we can handle this on our own. +def _patched_lookup_annotation(*_args: Any) -> str: + """ + GoogleDocstring._lookup_annotation sometimes adds incorrect type annotations to constructor parameters. + + 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 - """ - GoogleDocstring._lookup_annotation = patched_lookup_annotation # type: ignore[assignment] +def _patch_google_docstring_lookup_annotation() -> None: + """Fix issue https://github.com/tox-dev/sphinx-autodoc-typehints/issues/308.""" + GoogleDocstring._lookup_annotation = _patched_lookup_annotation # type: ignore[assignment] # noqa: SLF001 orig_base_admonition_run = BaseAdmonition.run -def patched_base_admonition_run(self: BaseAdmonition) -> Any: +def _patched_base_admonition_run(self: BaseAdmonition) -> Any: result = orig_base_admonition_run(self) result[0].line = self.lineno return result @@ -100,7 +99,7 @@ def patched_base_admonition_run(self: BaseAdmonition) -> Any: orig_text_indent = Text.indent -def patched_text_indent(self: Text, *args: Any) -> Any: +def _patched_text_indent(self: Text, *args: Any) -> Any: _, line = self.state_machine.get_source_and_line() result = orig_text_indent(self, *args) node = self.parent[-1] @@ -110,21 +109,29 @@ def patched_text_indent(self: Text, *args: Any) -> Any: return result -def patch_line_numbers() -> None: - """Make the rst parser put line numbers on more nodes. +def _patch_line_numbers() -> None: + """ + Make the rst parser put line numbers on more nodes. When the line numbers are missing, we have a hard time placing the :rtype:. """ - Text.indent = patched_text_indent - BaseAdmonition.run = patched_base_admonition_run + Text.indent = _patched_text_indent + BaseAdmonition.run = _patched_base_admonition_run def install_patches(app: Sphinx) -> None: + """ + Install the patches. + + :param app: the Sphinx app + """ fix_autodoc_typehints_for_overloaded_methods() patch_attribute_handling(app) - patch_google_docstring_lookup_annotation() + _patch_google_docstring_lookup_annotation() fix_napoleon_numpy_docstring_return_type(app) - patch_line_numbers() + _patch_line_numbers() -___all__ = ["install_patches"] +___all__ = [ + "install_patches", +] diff --git a/tests/conftest.py b/tests/conftest.py index 373a444c..9d37f025 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,17 +1,18 @@ from __future__ import annotations -import os import re import shutil import sys from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any import pytest -from _pytest.config import Config from sphinx.testing.path import path from sphobjinv import Inventory +if TYPE_CHECKING: + from _pytest.config import Config + pytest_plugins = "sphinx.testing.fixtures" collect_ignore = ["roots"] @@ -24,8 +25,7 @@ def inv(pytestconfig: Config) -> Inventory: if inv_dict is not None: return Inventory(inv_dict) - print("Downloading objects.inv") - url = "https://docs.python.org/{v.major}.{v.minor}/objects.inv".format(v=sys.version_info) + url = f"https://docs.python.org/{sys.version_info.major}.{sys.version_info.minor}/objects.inv" inv = Inventory(url=url) pytestconfig.cache.set(cache_path, inv.json_dict()) return inv @@ -47,10 +47,10 @@ def _remove_sphinx_projects(sphinx_test_tempdir: path) -> None: @pytest.fixture() def rootdir() -> path: - return path(os.path.dirname(__file__) or ".").abspath() / "roots" + return path(str(Path(__file__).parent) or ".").abspath() / "roots" -def pytest_ignore_collect(path: Any, config: Config) -> bool | None: # noqa: U100 +def pytest_ignore_collect(path: Any, config: Config) -> bool | None: # noqa: ARG001 version_re = re.compile(r"_py(\d)(\d)\.py$") match = version_re.search(path.basename) if match: diff --git a/tests/roots/test-dummy/conf.py b/tests/roots/test-dummy/conf.py index 14a5de11..079621cd 100644 --- a/tests/roots/test-dummy/conf.py +++ b/tests/roots/test-dummy/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pathlib import sys diff --git a/tests/roots/test-dummy/dummy_module.py b/tests/roots/test-dummy/dummy_module.py index 65d35a89..95197cd2 100644 --- a/tests/roots/test-dummy/dummy_module.py +++ b/tests/roots/test-dummy/dummy_module.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import dataclass diff --git a/tests/roots/test-dummy/dummy_module_future_annotations.py b/tests/roots/test-dummy/dummy_module_future_annotations.py index 5cc16cb1..103c8abd 100644 --- a/tests/roots/test-dummy/dummy_module_future_annotations.py +++ b/tests/roots/test-dummy/dummy_module_future_annotations.py @@ -2,7 +2,10 @@ def function_with_py310_annotations( - self, x: bool | None, y: int | str | float, z: str | None = None # noqa: U100 + self, # noqa: ANN001, ARG001 + x: bool | None, # noqa: ARG001 + y: int | str | float, # noqa: ARG001 + z: str | None = None, # noqa: ARG001 ) -> str: """ Method docstring. diff --git a/tests/roots/test-dummy/dummy_module_simple.py b/tests/roots/test-dummy/dummy_module_simple.py index 400c748a..fed8cdfc 100644 --- a/tests/roots/test-dummy/dummy_module_simple.py +++ b/tests/roots/test-dummy/dummy_module_simple.py @@ -1,4 +1,7 @@ -def function(x: bool, y: int = 1) -> str: # noqa: U100 +from __future__ import annotations + + +def function(x: bool, y: int = 1) -> str: # noqa: ARG001 """ Function docstring. diff --git a/tests/roots/test-dummy/dummy_module_simple_no_use_rtype.py b/tests/roots/test-dummy/dummy_module_simple_no_use_rtype.py index 3f10b4dc..8dc80e7f 100644 --- a/tests/roots/test-dummy/dummy_module_simple_no_use_rtype.py +++ b/tests/roots/test-dummy/dummy_module_simple_no_use_rtype.py @@ -1,4 +1,7 @@ -def function_no_returns(x: bool, y: int = 1) -> str: # noqa: U100 +from __future__ import annotations + + +def function_no_returns(x: bool, y: int = 1) -> str: # noqa: ARG001 """ Function docstring. @@ -7,7 +10,7 @@ def function_no_returns(x: bool, y: int = 1) -> str: # noqa: U100 """ -def function_returns_with_type(x: bool, y: int = 1) -> str: # noqa: U100 +def function_returns_with_type(x: bool, y: int = 1) -> str: # noqa: ARG001 """ Function docstring. @@ -17,7 +20,7 @@ def function_returns_with_type(x: bool, y: int = 1) -> str: # noqa: U100 """ -def function_returns_with_compound_type(x: bool, y: int = 1) -> str: # noqa: U100 +def function_returns_with_compound_type(x: bool, y: int = 1) -> str: # noqa: ARG001 """ Function docstring. @@ -27,7 +30,7 @@ def function_returns_with_compound_type(x: bool, y: int = 1) -> str: # noqa: U1 """ -def function_returns_without_type(x: bool, y: int = 1) -> str: # noqa: U100 +def function_returns_without_type(x: bool, y: int = 1) -> str: # noqa: ARG001 """ Function docstring. diff --git a/tests/roots/test-dummy/dummy_module_without_complete_typehints.py b/tests/roots/test-dummy/dummy_module_without_complete_typehints.py index 6d7e91ef..44ca0460 100644 --- a/tests/roots/test-dummy/dummy_module_without_complete_typehints.py +++ b/tests/roots/test-dummy/dummy_module_without_complete_typehints.py @@ -1,4 +1,7 @@ -def function_with_some_defaults_and_without_typehints(x, y=None): # noqa: U100 +from __future__ import annotations + + +def function_with_some_defaults_and_without_typehints(x, y=None): # noqa: ANN001, ANN201, ARG001 """ Function docstring. @@ -7,7 +10,7 @@ def function_with_some_defaults_and_without_typehints(x, y=None): # noqa: U100 """ -def function_with_some_defaults_and_some_typehints(x: int, y=None): # noqa: U100 +def function_with_some_defaults_and_some_typehints(x: int, y=None): # noqa: ANN001, ANN201, ARG001 """ Function docstring. @@ -16,7 +19,7 @@ def function_with_some_defaults_and_some_typehints(x: int, y=None): # noqa: U10 """ -def function_with_some_defaults_and_more_typehints(x: int, y=None) -> str: # noqa: U100 +def function_with_some_defaults_and_more_typehints(x: int, y=None) -> str: # noqa: ANN001, ARG001 """ Function docstring. @@ -25,7 +28,7 @@ def function_with_some_defaults_and_more_typehints(x: int, y=None) -> str: # no """ -def function_with_defaults_and_some_typehints(x: int = 0, y=None) -> str: # noqa: U100 +def function_with_defaults_and_some_typehints(x: int = 0, y=None) -> str: # noqa: ANN001, ARG001 """ Function docstring. diff --git a/tests/roots/test-integration/conf.py b/tests/roots/test-integration/conf.py index 14a5de11..079621cd 100644 --- a/tests/roots/test-integration/conf.py +++ b/tests/roots/test-integration/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pathlib import sys diff --git a/tests/roots/test-resolve-typing-guard-tmp/conf.py b/tests/roots/test-resolve-typing-guard-tmp/conf.py index bb939ea4..a327e5bd 100644 --- a/tests/roots/test-resolve-typing-guard-tmp/conf.py +++ b/tests/roots/test-resolve-typing-guard-tmp/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pathlib import sys diff --git a/tests/roots/test-resolve-typing-guard-tmp/demo_typing_guard.py b/tests/roots/test-resolve-typing-guard-tmp/demo_typing_guard.py index 392488aa..11c28a89 100644 --- a/tests/roots/test-resolve-typing-guard-tmp/demo_typing_guard.py +++ b/tests/roots/test-resolve-typing-guard-tmp/demo_typing_guard.py @@ -1,13 +1,10 @@ """Module demonstrating imports that are type guarded""" from __future__ import annotations -from typing import TYPE_CHECKING +import datetime from attrs import define -if TYPE_CHECKING: - import datetime - @define() class SomeClass: @@ -17,7 +14,7 @@ class SomeClass: """Date to handle""" @classmethod - def from_str(cls, input_value: str) -> SomeClass: + def from_str(cls, input_value: str) -> SomeClass: # noqa: ANN102 """ Initialise from string @@ -27,7 +24,7 @@ def from_str(cls, input_value: str) -> SomeClass: return cls(input_value) @classmethod - def from_date(cls, input_value: datetime.date) -> SomeClass: + def from_date(cls, input_value: datetime.date) -> SomeClass: # noqa: ANN102 """ Initialise from date @@ -37,7 +34,7 @@ def from_date(cls, input_value: datetime.date) -> SomeClass: return cls(input_value) @classmethod - def from_time(cls, input_value: datetime.time) -> SomeClass: + def from_time(cls, input_value: datetime.time) -> SomeClass: # noqa: ANN102 """ Initialise from time diff --git a/tests/roots/test-resolve-typing-guard/conf.py b/tests/roots/test-resolve-typing-guard/conf.py index bb939ea4..a327e5bd 100644 --- a/tests/roots/test-resolve-typing-guard/conf.py +++ b/tests/roots/test-resolve-typing-guard/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pathlib import sys diff --git a/tests/roots/test-resolve-typing-guard/demo_typing_guard.py b/tests/roots/test-resolve-typing-guard/demo_typing_guard.py index fe120ad7..70324466 100644 --- a/tests/roots/test-resolve-typing-guard/demo_typing_guard.py +++ b/tests/roots/test-resolve-typing-guard/demo_typing_guard.py @@ -36,7 +36,7 @@ class SomeClass: if TYPE_CHECKING: # Classes doesn't have `__globals__` attribute - def __getattr__(self, item: str): # noqa: U100 + def __getattr__(self, item: str): # noqa: ANN204 """This method do something.""" diff --git a/tests/roots/test-resolve-typing-guard/demo_typing_guard_dummy.py b/tests/roots/test-resolve-typing-guard/demo_typing_guard_dummy.py index 43964e24..f06dfbc7 100644 --- a/tests/roots/test-resolve-typing-guard/demo_typing_guard_dummy.py +++ b/tests/roots/test-resolve-typing-guard/demo_typing_guard_dummy.py @@ -1,4 +1,6 @@ -from viktor import AI # module part of autodoc_mock_imports # noqa: F401,SC100,SC200 +from __future__ import annotations + +from viktor import AI # module part of autodoc_mock_imports # noqa: F401 class AnotherClass: diff --git a/tests/test_integration.py b/tests/test_integration.py index ace46091..8907d454 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,16 +1,21 @@ +from __future__ import annotations + 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, indent -from types import CodeType, ModuleType -from typing import Any, Callable, Optional, TypeVar, Union, overload +from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union, overload # no type comments import pytest -from sphinx.testing.util import SphinxTestApp + +if TYPE_CHECKING: + from io import StringIO + from mailbox import Mailbox + from types import CodeType, ModuleType + + from sphinx.testing.util import SphinxTestApp T = TypeVar("T") @@ -32,8 +37,8 @@ def dec(val: T) -> T: @expected("mod.get_local_function()") -def get_local_function(): - def wrapper(self) -> str: +def get_local_function(): # noqa: ANN201 + def wrapper(self) -> str: # noqa: ANN001, ARG001 """ Wrapper """ @@ -121,7 +126,7 @@ class InnerClass Return type: "str" - """ + """, ) class Class: """ @@ -132,10 +137,10 @@ class Class: :param z: baz """ - def __init__(self, x: bool, y: int, z: Optional[str] = None) -> None: # noqa: U100 + def __init__(self, x: bool, y: int, z: Optional[str] = None) -> None: # noqa: UP007 pass - def a_method(self, x: bool, y: int, z: Optional[str] = None) -> str: # noqa: U100 + def a_method(self, x: bool, y: int, z: Optional[str] = None) -> str: # noqa: UP007 """ Method docstring. @@ -144,21 +149,21 @@ def a_method(self, x: bool, y: int, z: Optional[str] = None) -> str: # noqa: U1 :param z: baz """ - def _private_method(self, x: str) -> str: # noqa: U100 + def _private_method(self, x: str) -> str: """ Private method docstring. :param x: foo """ - def __dunder_method(self, x: str) -> str: # noqa: U100 + def __dunder_method(self, x: str) -> str: """ Dunder method docstring. :param x: foo """ - def __magic_custom_method__(self, x: str) -> str: # noqa: U100 + def __magic_custom_method__(self, x: str) -> str: """ Magic dunder method docstring. @@ -166,7 +171,7 @@ def __magic_custom_method__(self, x: str) -> str: # noqa: U100 """ @classmethod - def a_classmethod(cls, x: bool, y: int, z: Optional[str] = None) -> str: # noqa: U100 + def a_classmethod(cls, x: bool, y: int, z: Optional[str] = None) -> str: # noqa: ANN102, UP007 """ Classmethod docstring. @@ -176,7 +181,7 @@ def a_classmethod(cls, x: bool, y: int, z: Optional[str] = None) -> str: # noqa """ @staticmethod - def a_staticmethod(x: bool, y: int, z: Optional[str] = None) -> str: # noqa: U100 + def a_staticmethod(x: bool, y: int, z: Optional[str] = None) -> str: # noqa: UP007 """ Staticmethod docstring. @@ -196,14 +201,14 @@ class InnerClass: Inner class. """ - def inner_method(self, x: bool) -> str: # noqa: U100 + def inner_method(self, x: bool) -> str: """ Inner method. :param x: foo """ - def __dunder_inner_method(self, x: bool) -> str: # noqa: U100 + def __dunder_inner_method(self, x: bool) -> str: """ Dunder inner method. @@ -221,7 +226,7 @@ def __dunder_inner_method(self, x: bool) -> str: # noqa: U100 Parameters: **message** ("str") -- blah -""" +""", ) class DummyException(Exception): # noqa: N818 """ @@ -252,9 +257,9 @@ def __init__(self, message: str) -> None: Return type: bytes -""" +""", ) -def function(x: bool, y: int, z_: Optional[str] = None) -> str: # noqa: U100 +def function(x: bool, y: int, z_: Optional[str] = None) -> str: # noqa: ARG001, UP007 """ Function docstring. @@ -280,9 +285,9 @@ def function(x: bool, y: int, z_: Optional[str] = None) -> str: # noqa: U100 * ***args** ("int") -- foo * ****kwargs** ("str") -- bar -""" +""", ) -def function_with_starred_documentation_param_names(*args: int, **kwargs: str): # noqa: U100 +def function_with_starred_documentation_param_names(*args: int, **kwargs: str): # noqa: ANN201, ARG001 r""" Function docstring. @@ -303,9 +308,9 @@ def function_with_starred_documentation_param_names(*args: int, **kwargs: str): Parameters: **x** ("str") -- foo -""" +""", ) -def function_with_escaped_default(x: str = "\b"): # noqa: U100 +def function_with_escaped_default(x: str = "\b"): # noqa: ANN201, ARG001 """ Function docstring. @@ -322,9 +327,9 @@ def function_with_escaped_default(x: str = "\b"): # noqa: U100 Parameters: **x** (*a.b.c*) -- foo -""" +""", ) -def function_with_unresolvable_annotation(x: "a.b.c"): # noqa: U100,F821 +def function_with_unresolvable_annotation(x: a.b.c): # noqa: ANN201, ARG001, F821 """ Function docstring. @@ -345,11 +350,11 @@ def function_with_unresolvable_annotation(x: "a.b.c"): # noqa: U100,F821 Return type: "None" -""" +""", ) -def function_with_typehint_comment( - x, # type: int # noqa: U100 - y, # type: str # noqa: U100 +def function_with_typehint_comment( # noqa: ANN201 + x, # type: int # noqa: ANN001, ARG001 + y, # type: str # noqa: ANN001, ARG001 ): # type: (...) -> None """ @@ -382,7 +387,7 @@ class mod.ClassWithTypehints(x) method_without_typehint(x) Method docstring. -""" +""", ) class ClassWithTypehints: """ @@ -392,13 +397,15 @@ class ClassWithTypehints: """ def __init__( - self, x # type: int # noqa: U100 - ): + self, + x, # type: int # noqa: ANN001 + ) -> None: # type: (...) -> None pass - def foo( - self, x # type: str # noqa: U100 + def foo( # noqa: ANN201 + self, + x, # type: str # noqa: ANN001, ARG002 ): # type: (...) -> int """ @@ -408,7 +415,7 @@ def foo( """ return 42 - def method_without_typehint(self, x): # noqa: U100 + def method_without_typehint(self, x): # noqa: ANN001, ANN201, ARG002 """ Method docstring. """ @@ -416,7 +423,7 @@ def method_without_typehint(self, x): # noqa: U100 multiline_str = """ test """ - return multiline_str + return multiline_str # noqa: RET504 @expected( @@ -436,9 +443,9 @@ def method_without_typehint(self, x): # noqa: U100 Return type: "None" -""" +""", ) -def function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs): # noqa: U100 +def function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs): # noqa: ANN001, ANN002, ANN003, ANN201, ARG001 # type: (Union[str, bytes, None], *str, bytes, **int) -> None """ Function docstring. @@ -479,7 +486,7 @@ class mod.ClassWithTypehintsNotInline(x=None) Return type: "ClassWithTypehintsNotInline" -""" +""", ) class ClassWithTypehintsNotInline: """ @@ -488,12 +495,10 @@ class ClassWithTypehintsNotInline: :param x: foo """ - def __init__(self, x=None): # noqa: U100 - # type: (Optional[Callable[[int, bytes], int]]) -> None + def __init__(self, x=None) -> None: # type: (Optional[Callable[[int, bytes], int]]) -> None # noqa: ANN001 pass - def foo(self, x=1): - # type: (Callable[[int, bytes], int]) -> int + def foo(self, x=1): # type: (Callable[[int, bytes], int]) -> int # noqa: ANN001, ANN201 """ Method docstring. @@ -502,8 +507,10 @@ def foo(self, x=1): return x(1, b"") @classmethod - def mk(cls, x=None): - # type: (Optional[Callable[[int, bytes], int]]) -> ClassWithTypehintsNotInline + def mk( # noqa: ANN206 + cls, # noqa: ANN102 + x=None, # noqa: ANN001 + ): # type: (Optional[Callable[[int, bytes], int]]) -> ClassWithTypehintsNotInline """ Method docstring. @@ -520,7 +527,7 @@ def mk(cls, x=None): Return type: "str" -""" +""", ) def undocumented_function(x: int) -> str: """Hi""" @@ -533,7 +540,7 @@ def undocumented_function(x: int) -> str: class mod.DataClass(x) Class docstring. -""" +""", ) @dataclass class DataClass: @@ -550,7 +557,7 @@ class mod.Decorator(func) Parameters: **func** ("Callable"[["int", "str"], "str"]) -- function -""" +""", ) class Decorator: """ @@ -559,7 +566,7 @@ class Decorator: :param func: function """ - def __init__(self, func: Callable[[int, str], str]): # noqa: U100 + def __init__(self, func: Callable[[int, str], str]) -> None: pass @@ -571,9 +578,9 @@ def __init__(self, func: Callable[[int, str], str]): # noqa: U100 Parameters: **x** ("Mailbox") -- function -""" +""", ) -def mocked_import(x: Mailbox): # noqa: U100 +def mocked_import(x: Mailbox): # noqa: ANN201, ARG001 """ A docstring. @@ -593,7 +600,7 @@ def mocked_import(x: Mailbox): # noqa: U100 -[ Examples ]- Here are a couple of examples of how to use this function. -""" +""", ) def func_with_examples() -> int: """ @@ -606,12 +613,12 @@ def func_with_examples() -> int: @overload -def func_with_overload(a: int, b: int) -> None: # noqa: U100 +def func_with_overload(a: int, b: int) -> None: ... @overload -def func_with_overload(a: str, b: str) -> None: # noqa: U100 +def func_with_overload(a: str, b: str) -> None: ... @@ -629,9 +636,9 @@ def func_with_overload(a: str, b: str) -> None: # noqa: U100 Return type: "None" -""" +""", ) -def func_with_overload(a: Union[int, str], b: Union[int, str]) -> None: # noqa: U100 +def func_with_overload(a: Union[int, str], b: Union[int, str]) -> None: # noqa: ARG001, UP007 """ f does the thing. The arguments can either be ints or strings but they must both have the same type. @@ -654,12 +661,12 @@ class mod.TestClassAttributeDocs code: "Optional"["CodeType"] An attribute -""" +""", ) class TestClassAttributeDocs: """A class""" - code: Union[CodeType, None] + code: Optional[CodeType] # noqa: UP007 """An attribute""" @@ -678,7 +685,7 @@ class TestClassAttributeDocs: Returns: The index of the widget -""" +""", ) def func_with_examples_and_returns_after() -> int: """ @@ -708,9 +715,9 @@ def func_with_examples_and_returns_after() -> int: "int" More info about the function here. -""" +""", ) -def func_with_parameters_and_stuff_after(a: int, b: int) -> int: # noqa: U100 +def func_with_parameters_and_stuff_after(a: int, b: int) -> int: # noqa: ARG001 """A func :param a: a tells us something @@ -742,9 +749,9 @@ def func_with_parameters_and_stuff_after(a: int, b: int) -> int: # noqa: U100 Return type: int -""" +""", ) -def func_with_rtype_in_weird_spot(a: int, b: int) -> int: # noqa: U100 +def func_with_rtype_in_weird_spot(a: int, b: int) -> int: # noqa: ARG001 """A func :param a: a tells us something @@ -790,9 +797,9 @@ def func_with_rtype_in_weird_spot(a: int, b: int) -> int: # noqa: U100 "int" More stuff here. -""" +""", ) -def empty_line_between_parameters(a: int, b: int) -> int: # noqa: U100 +def empty_line_between_parameters(a: int, b: int) -> int: # noqa: ARG001 """A func :param a: One of the following possibilities: @@ -827,7 +834,7 @@ def empty_line_between_parameters(a: int, b: int) -> int: # noqa: U100 -[ Examples ]- Here are a couple of examples of how to use this function. -""" +""", ) def func_with_code_block() -> int: """ @@ -860,7 +867,7 @@ def func_with_code_block() -> int: xyz something - """ + """, ) def func_with_definition_list() -> int: """Some text and then a definition list. @@ -889,7 +896,7 @@ def func_with_definition_list() -> int: -[ Examples ]- A -""" +""", ) def decorator_2(f: Any) -> Any: """Run the decorated function with `asyncio.run`. @@ -906,7 +913,7 @@ def decorator_2(f: Any) -> Any: A """ - f + assert f is not None @expected( @@ -922,7 +929,7 @@ class mod.ParamAndAttributeHaveSameName(blah) Description of attribute blah - """ + """, ) class ParamAndAttributeHaveSameName: """ @@ -934,7 +941,7 @@ class ParamAndAttributeHaveSameName: Description of parameter blah """ - def __init__(self, blah: CodeType): # noqa: U100 + def __init__(self, blah: CodeType) -> None: pass blah: ModuleType @@ -952,7 +959,7 @@ def __init__(self, blah: CodeType): # noqa: U100 Returns: The info about the whatever. - """ + """, ) def napoleon_returns() -> CodeType: """ @@ -983,9 +990,9 @@ def napoleon_returns() -> CodeType: Returns: Description of return value - """ + """, ) -def google_docstrings(arg1: CodeType, arg2: ModuleType) -> CodeType: # noqa: U100 +def google_docstrings(arg1: CodeType, arg2: ModuleType) -> CodeType: # noqa: ARG001 """Summary line. Extended description of function. @@ -1015,9 +1022,9 @@ def google_docstrings(arg1: CodeType, arg2: ModuleType) -> CodeType: # noqa: U1 Some notes. More notes - """ + """, ) -def docstring_with_multiline_note_after_params(param: int) -> None: # noqa: U100 +def docstring_with_multiline_note_after_params(param: int) -> None: # noqa: ARG001 """Do something. Args: @@ -1046,9 +1053,9 @@ def docstring_with_multiline_note_after_params(param: int) -> None: # noqa: U10 * C: D - """ + """, ) -def docstring_with_bullet_list_after_params(param: int) -> None: # noqa: U100 +def docstring_with_bullet_list_after_params(param: int) -> None: # noqa: ARG001 """Do something. Args: @@ -1079,9 +1086,9 @@ def docstring_with_bullet_list_after_params(param: int) -> None: # noqa: U100 Next Term Somethign about it - """ + """, ) -def docstring_with_definition_list_after_params(param: int) -> None: # noqa: U100 +def docstring_with_definition_list_after_params(param: int) -> None: # noqa: ARG001 """Do something. Args: @@ -1113,9 +1120,9 @@ def docstring_with_definition_list_after_params(param: int) -> None: # noqa: U1 2. C: D - """ + """, ) -def docstring_with_enum_list_after_params(param: int) -> None: # noqa: U100 +def docstring_with_enum_list_after_params(param: int) -> None: # noqa: ARG001 """Do something. Args: @@ -1148,9 +1155,9 @@ def docstring_with_enum_list_after_params(param: int) -> None: # noqa: U100 Somethign about it -[ Example ]- - """ + """, ) -def docstring_with_definition_list_after_params_no_blank_line(param: int) -> None: # noqa: U100 +def docstring_with_definition_list_after_params_no_blank_line(param: int) -> None: # noqa: ARG001 """Do something. Args: @@ -1178,22 +1185,28 @@ def docstring_with_definition_list_after_params_no_blank_line(param: int) -> Non """ -@pytest.mark.parametrize("object", [x for x in globals().values() if hasattr(x, "EXPECTED")]) +@pytest.mark.parametrize("val", [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): +def test_integration( + app: SphinxTestApp, + status: StringIO, + warning: StringIO, + monkeypatch: pytest.MonkeyPatch, + val: Any, +) -> None: + if isclass(val) and issubclass(val, BaseException): template = AUTO_EXCEPTION - elif isclass(object): + elif isclass(val): template = AUTO_CLASS else: template = AUTO_FUNCTION - (Path(app.srcdir) / "index.rst").write_text(template.format(object.__name__)) + (Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__)) monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__]) app.build() assert "build succeeded" in status.getvalue() # Build succeeded - regexp = getattr(object, "WARNING", None) + regexp = getattr(val, "WARNING", None) value = warning.getvalue().strip() if regexp: msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}" @@ -1203,10 +1216,10 @@ def test_integration(app: SphinxTestApp, status: StringIO, warning: StringIO, mo result = (Path(app.srcdir) / "_build/text/index.txt").read_text() - expected = object.EXPECTED + expected = val.EXPECTED try: assert result.strip() == dedent(expected).strip() except Exception: indented = indent(f'"""\n{result}\n"""', " " * 4) - print(f"@expected(\n{indented}\n)\n") + print(f"@expected(\n{indented}\n)\n") # noqa: T201 raise diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index 7bc9892b..f964f6e6 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -35,9 +35,6 @@ import typing_extensions from sphinx.application import Sphinx from sphinx.config import Config -from sphinx.testing.util import SphinxTestApp -from sphobjinv import Inventory - from sphinx_autodoc_typehints import ( _resolve_type_guarded_imports, backfill_type_hints, @@ -49,20 +46,24 @@ process_docstring, ) +if typing.TYPE_CHECKING: + from sphinx.testing.util import SphinxTestApp + from sphobjinv import Inventory + T = TypeVar("T") U = TypeVar("U", covariant=True) V = TypeVar("V", contravariant=True) X = TypeVar("X", str, int) Y = TypeVar("Y", bound=str) Z = TypeVar("Z", bound="A") -S = TypeVar("S", bound="miss") # type: ignore # miss not defined on purpose # noqa: F821 +S = TypeVar("S", bound="miss") # type: ignore[name-defined] # miss not defined on purpose # noqa: F821 W = NewType("W", str) P = typing_extensions.ParamSpec("P") P_args = P.args # type:ignore[attr-defined] P_kwargs = P.kwargs # type:ignore[attr-defined] -P_co = typing_extensions.ParamSpec("P_co", covariant=True) # type: ignore -P_contra = typing_extensions.ParamSpec("P_contra", contravariant=True) # type: ignore -P_bound = typing_extensions.ParamSpec("P_bound", bound=str) # type: ignore +P_co = typing_extensions.ParamSpec("P_co", covariant=True) # type: ignore[misc] +P_contra = typing_extensions.ParamSpec("P_contra", contravariant=True) # type: ignore[misc] +P_bound = typing_extensions.ParamSpec("P_bound", bound=str) # type: ignore[misc] # Mypy does not support recursive type aliases, but # other type checkers do. @@ -91,7 +92,7 @@ class D(typing_extensions.Protocol): ... -class E(typing_extensions.Protocol[T]): # type: ignore # Invariant type variable "T" used in protocol where covariant +class E(typing_extensions.Protocol[T]): # type: ignore[misc] ... @@ -105,10 +106,10 @@ class Metaclass(type): class HintedMethods: @classmethod - def from_magic(cls: type[T]) -> T: # type: ignore + def from_magic(cls: type[T]) -> T: # type: ignore[empty-body] ... - def method(self: T) -> T: # type: ignore + def method(self: T) -> T: # type: ignore[empty-body] ... @@ -117,11 +118,10 @@ def method(self: T) -> T: # type: ignore if sys.version_info >= (3, 9): AbcCallable = collections.abc.Callable # type: ignore[type-arg] else: - # Hacks to make it work the same in old versions. # We could also set AbcCallable = typing.Callable and x fail the tests that # use AbcCallable when in versions less than 3.9. - class MyGenericAlias(typing._VariadicGenericAlias, _root=True): # noqa: SC200 - def __getitem__(self, params): + class MyGenericAlias(typing._VariadicGenericAlias, _root=True): # noqa: SLF001 + def __getitem__(self, params): # noqa: ANN001, ANN204 result = super().__getitem__(params) # Make a copy so we don't change the name of a cached annotation result = result.copy_with(result.__args__) @@ -153,7 +153,7 @@ def __getitem__(self, params): pytest.param(Callable[..., str], "typing", "Callable", (..., str), id="Callable_returntype"), pytest.param(Callable[[int, str], str], "typing", "Callable", (int, str, str), id="Callable_all_types"), pytest.param( - AbcCallable[[int, str], str], # type: ignore + AbcCallable[[int, str], str], # type: ignore[type-arg,misc,valid-type] "collections.abc", "Callable", (int, str, str), @@ -216,7 +216,7 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t ":py:class:`~typing.Mapping`\\[:py:class:`~typing.TypeVar`\\(``T``), " ":py:class:`~typing.TypeVar`\\(``U``, covariant=True)]", ), - (Mapping[str, bool], ":py:class:`~typing.Mapping`\\[:py:class:`str`, " ":py:class:`bool`]"), + (Mapping[str, bool], ":py:class:`~typing.Mapping`\\[:py:class:`str`, :py:class:`bool`]"), (Dict, ":py:class:`~typing.Dict`"), ( Dict[T, int], # type: ignore[valid-type] @@ -231,37 +231,38 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t ":py:class:`~typing.Dict`\\[:py:class:`~typing.TypeVar`\\(``T``)," " :py:class:`~typing.TypeVar`\\(``U``, covariant=True)]", ), - (Dict[str, bool], ":py:class:`~typing.Dict`\\[:py:class:`str`, " ":py:class:`bool`]"), + (Dict[str, bool], ":py:class:`~typing.Dict`\\[:py:class:`str`, :py:class:`bool`]"), (Tuple, ":py:data:`~typing.Tuple`"), - (Tuple[str, bool], ":py:data:`~typing.Tuple`\\[:py:class:`str`, " ":py:class:`bool`]"), - (Tuple[int, int, int], ":py:data:`~typing.Tuple`\\[:py:class:`int`, " ":py:class:`int`, :py:class:`int`]"), + (Tuple[str, bool], ":py:data:`~typing.Tuple`\\[:py:class:`str`, :py:class:`bool`]"), + (Tuple[int, int, int], ":py:data:`~typing.Tuple`\\[:py:class:`int`, :py:class:`int`, :py:class:`int`]"), (Tuple[str, ...], ":py:data:`~typing.Tuple`\\[:py:class:`str`, :py:data:`...`]"), (Union, ":py:data:`~typing.Union`"), - (Union[str, bool], ":py:data:`~typing.Union`\\[:py:class:`str`, " ":py:class:`bool`]"), - (Union[str, bool, None], ":py:data:`~typing.Union`\\[:py:class:`str`, " ":py:class:`bool`, :py:obj:`None`]"), + (Union[str, bool], ":py:data:`~typing.Union`\\[:py:class:`str`, :py:class:`bool`]"), + (Union[str, bool, None], ":py:data:`~typing.Union`\\[:py:class:`str`, :py:class:`bool`, :py:obj:`None`]"), pytest.param( Union[str, Any], - ":py:data:`~typing.Union`\\[:py:class:`str`, " ":py:data:`~typing.Any`]", + ":py:data:`~typing.Union`\\[:py:class:`str`, :py:data:`~typing.Any`]", marks=pytest.mark.skipif( - (3, 5, 0) <= sys.version_info[:3] <= (3, 5, 2), reason="Union erases the str on 3.5.0 -> 3.5.2" + (3, 5, 0) <= sys.version_info[:3] <= (3, 5, 2), + reason="Union erases the str on 3.5.0 -> 3.5.2", ), ), (Optional[str], ":py:data:`~typing.Optional`\\[:py:class:`str`]"), (Union[str, None], ":py:data:`~typing.Optional`\\[:py:class:`str`]"), ( Optional[Union[str, bool]], - ":py:data:`~typing.Union`\\[:py:class:`str`, " ":py:class:`bool`, :py:obj:`None`]", + ":py:data:`~typing.Union`\\[:py:class:`str`, :py:class:`bool`, :py:obj:`None`]", ), (Callable, ":py:data:`~typing.Callable`"), (Callable[..., int], ":py:data:`~typing.Callable`\\[:py:data:`...`, :py:class:`int`]"), - (Callable[[int], int], ":py:data:`~typing.Callable`\\[\\[:py:class:`int`], " ":py:class:`int`]"), + (Callable[[int], int], ":py:data:`~typing.Callable`\\[\\[:py:class:`int`], :py:class:`int`]"), ( Callable[[int, str], bool], - ":py:data:`~typing.Callable`\\[\\[:py:class:`int`, " ":py:class:`str`], :py:class:`bool`]", + ":py:data:`~typing.Callable`\\[\\[:py:class:`int`, :py:class:`str`], :py:class:`bool`]", ), ( Callable[[int, str], None], - ":py:data:`~typing.Callable`\\[\\[:py:class:`int`, " ":py:class:`str`], :py:obj:`None`]", + ":py:data:`~typing.Callable`\\[\\[:py:class:`int`, :py:class:`str`], :py:obj:`None`]", ), ( Callable[[T], T], @@ -269,8 +270,8 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t " :py:class:`~typing.TypeVar`\\(``T``)]", ), ( - AbcCallable[[int, str], bool], # type: ignore - ":py:class:`~collections.abc.Callable`\\[\\[:py:class:`int`, " ":py:class:`str`], :py:class:`bool`]", + AbcCallable[[int, str], bool], # type: ignore[valid-type,misc,type-arg] + ":py:class:`~collections.abc.Callable`\\[\\[:py:class:`int`, :py:class:`str`], :py:class:`bool`]", ), (Pattern, ":py:class:`~typing.Pattern`"), (Pattern[str], ":py:class:`~typing.Pattern`\\[:py:class:`str`]"), @@ -284,7 +285,7 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t (D, ":py:class:`~%s.D`" % __name__), (E, ":py:class:`~%s.E`" % __name__), (E[int], ":py:class:`~%s.E`\\[:py:class:`int`]" % __name__), - (W, f':py:{"class" if PY310_PLUS else "func"}:' f"`~typing.NewType`\\(``W``, :py:class:`str`)"), + (W, f':py:{"class" if PY310_PLUS else "func"}:' f"`~typing.NewType`\\(``W``, :py:class:`str`)"), # noqa: ISC001 (T, ":py:class:`~typing.TypeVar`\\(``T``)"), (U, ":py:class:`~typing.TypeVar`\\(``U``, covariant=True)"), (V, ":py:class:`~typing.TypeVar`\\(``V``, contravariant=True)"), @@ -376,7 +377,10 @@ def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str if "typing" in expected_result_not_simplified: expected_result_not_simplified = expected_result_not_simplified.replace("~typing", "typing") conf = create_autospec( - Config, typehints_fully_qualified=True, simplify_optional_unions=False, _annotation_globals=globals() + Config, + typehints_fully_qualified=True, + simplify_optional_unions=False, + _annotation_globals=globals(), ) assert format_annotation(annotation, conf) == expected_result_not_simplified @@ -450,12 +454,15 @@ def set_python_path() -> None: @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_always_document_param_types( - app: SphinxTestApp, status: StringIO, warning: StringIO, always_document_param_types: bool + 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 + app.config.always_document_param_types = always_document_param_types # type: ignore[attr-defined] # create flag + app.config.autodoc_mock_imports = ["mailbox"] # type: ignore[attr-defined] # create flag # Prevent "document isn't included in any toctree" warnings for f in Path(app.srcdir).glob("*.rst"): @@ -468,8 +475,8 @@ def test_always_document_param_types( .. autoclass:: dummy_module.DataClass :undoc-members: :special-members: __init__ - """ - ) + """, + ), ) app.build() @@ -526,7 +533,7 @@ def maybe_fix_py310(expected_contents: str) -> str: def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) -> None: set_python_path() - app.config.master_doc = "future_annotations" # type: ignore # create flag + app.config.master_doc = "future_annotations" # type: ignore[attr-defined] # create flag app.build() assert "build succeeded" in status.getvalue() # Build succeeded @@ -567,19 +574,20 @@ def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_defaults( - app: SphinxTestApp, status: StringIO, defaults_config_val: str, expected: str | Exception + app: SphinxTestApp, + status: StringIO, + defaults_config_val: str, + expected: str | Exception, ) -> None: set_python_path() - app.config.master_doc = "simple" # type: ignore # create flag - app.config.typehints_defaults = defaults_config_val # type: ignore # create flag - try: - app.build() - except Exception as e: - if not isinstance(expected, Exception): - raise - assert str(expected) in str(e) + app.config.master_doc = "simple" # type: ignore[attr-defined] # create flag + app.config.typehints_defaults = defaults_config_val # type: ignore[attr-defined] # create flag + if isinstance(expected, Exception): + with pytest.raises(Exception, match=re.escape(str(expected))): + app.build() return + app.build() assert "build succeeded" in status.getvalue() contents = (Path(app.srcdir) / "_build/text/simple.txt").read_text() @@ -606,27 +614,27 @@ def test_sphinx_output_defaults( ("formatter_config_val", "expected"), [ (None, ['("bool") -- foo', '("int") -- bar', '"str"']), - (lambda ann, conf: "Test", ["(*Test*) -- foo", "(*Test*) -- bar", "Test"]), + (lambda ann, conf: "Test", ["(*Test*) -- foo", "(*Test*) -- bar", "Test"]), # noqa: ARG005 ("some string", Exception("needs to be callable or `None`")), ], ) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_formatter( - app: SphinxTestApp, status: StringIO, formatter_config_val: str, expected: tuple[str, ...] | Exception + app: SphinxTestApp, + status: StringIO, + formatter_config_val: str, + expected: tuple[str, ...] | Exception, ) -> None: set_python_path() - app.config.master_doc = "simple" # type: ignore # create flag - app.config.typehints_formatter = formatter_config_val # type: ignore # create flag - try: - app.build() - except Exception as e: - if not isinstance(expected, Exception): - raise - assert str(expected) in str(e) + app.config.master_doc = "simple" # type: ignore[attr-defined] # create flag + app.config.typehints_formatter = formatter_config_val # type: ignore[attr-defined] # create flag + if isinstance(expected, Exception): + with pytest.raises(Exception, match=re.escape(str(expected))): + app.build() return - assert not isinstance(expected, Exception), "Expected app.build() to raise exception, but it didn’t" + app.build() assert "build succeeded" in status.getvalue() contents = (Path(app.srcdir) / "_build/text/simple.txt").read_text() @@ -735,8 +743,8 @@ def test_bound_class_method(method: FunctionType) -> None: def test_syntax_error_backfill() -> None: # Regression test for #188 # fmt: off - func = ( # Note: line break here is what previously led to SyntaxError in process_docstring - lambda x: x) + def func(x): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202 + return x # fmt: on backfill_type_hints(func, "func") @@ -744,7 +752,7 @@ def test_syntax_error_backfill() -> None: @pytest.mark.sphinx("text", testroot="resolve-typing-guard") def test_resolve_typing_guard_imports(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: set_python_path() - app.config.autodoc_mock_imports = ["viktor"] # type: ignore # create flag + app.config.autodoc_mock_imports = ["viktor"] # type: ignore[attr-defined] # create flag app.build() assert "build succeeded" in status.getvalue() err = warning.getvalue() @@ -772,12 +780,12 @@ def test_no_source_code_type_guard() -> None: @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_formatter_no_use_rtype(app: SphinxTestApp, status: StringIO) -> None: set_python_path() - app.config.master_doc = "simple_no_use_rtype" # type: ignore # create flag - app.config.typehints_use_rtype = False # type: ignore + app.config.master_doc = "simple_no_use_rtype" # type: ignore[attr-defined] # create flag + app.config.typehints_use_rtype = False # type: ignore[attr-defined] app.build() assert "build succeeded" in status.getvalue() text_path = Path(app.srcdir) / "_build" / "text" / "simple_no_use_rtype.txt" - text_contents = text_path.read_text().replace("–", "--") + text_contents = text_path.read_text().replace("–", "--") # noqa: RUF001 # keep ambiguous EN DASH expected_contents = """\ Simple Module ************* @@ -837,12 +845,12 @@ def test_sphinx_output_formatter_no_use_rtype(app: SphinxTestApp, status: String @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_with_use_signature(app: SphinxTestApp, status: StringIO) -> None: set_python_path() - app.config.master_doc = "simple" # type: ignore # create flag - app.config.typehints_use_signature = True # type: ignore + app.config.master_doc = "simple" # type: ignore[attr-defined] # create flag + app.config.typehints_use_signature = True # type: ignore[attr-defined] app.build() assert "build succeeded" in status.getvalue() text_path = Path(app.srcdir) / "_build" / "text" / "simple.txt" - text_contents = text_path.read_text().replace("–", "--") + text_contents = text_path.read_text().replace("–", "--") # noqa: RUF001 # keep ambiguous EN DASH expected_contents = """\ Simple Module ************* @@ -866,12 +874,12 @@ def test_sphinx_output_with_use_signature(app: SphinxTestApp, status: StringIO) @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_with_use_signature_return(app: SphinxTestApp, status: StringIO) -> None: set_python_path() - app.config.master_doc = "simple" # type: ignore # create flag - app.config.typehints_use_signature_return = True # type: ignore + app.config.master_doc = "simple" # type: ignore[attr-defined] # create flag + app.config.typehints_use_signature_return = True # type: ignore[attr-defined] app.build() assert "build succeeded" in status.getvalue() text_path = Path(app.srcdir) / "_build" / "text" / "simple.txt" - text_contents = text_path.read_text().replace("–", "--") + text_contents = text_path.read_text().replace("–", "--") # noqa: RUF001 # keep ambiguous EN DASH expected_contents = """\ Simple Module ************* @@ -895,13 +903,13 @@ def test_sphinx_output_with_use_signature_return(app: SphinxTestApp, status: Str @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_with_use_signature_and_return(app: SphinxTestApp, status: StringIO) -> None: set_python_path() - app.config.master_doc = "simple" # type: ignore # create flag - app.config.typehints_use_signature = True # type: ignore - app.config.typehints_use_signature_return = True # type: ignore + app.config.master_doc = "simple" # type: ignore[attr-defined] # create flag + app.config.typehints_use_signature = True # type: ignore[attr-defined] + app.config.typehints_use_signature_return = True # type: ignore[attr-defined] app.build() assert "build succeeded" in status.getvalue() text_path = Path(app.srcdir) / "_build" / "text" / "simple.txt" - text_contents = text_path.read_text().replace("–", "--") + text_contents = text_path.read_text().replace("–", "--") # noqa: RUF001 # keep ambiguous EN DASH expected_contents = """\ Simple Module ************* @@ -925,12 +933,12 @@ def test_sphinx_output_with_use_signature_and_return(app: SphinxTestApp, status: @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_default_annotation_without_typehints(app: SphinxTestApp, status: StringIO) -> None: set_python_path() - app.config.master_doc = "without_complete_typehints" # type: ignore # create flag - app.config.typehints_defaults = "comma" # type: ignore + app.config.master_doc = "without_complete_typehints" # type: ignore[attr-defined]# create flag + app.config.typehints_defaults = "comma" # type: ignore[attr-defined] app.build() assert "build succeeded" in status.getvalue() text_path = Path(app.srcdir) / "_build" / "text" / "without_complete_typehints.txt" - text_contents = text_path.read_text().replace("–", "--") + text_contents = text_path.read_text().replace("–", "--") # noqa: RUF001 # keep ambiguous EN DASH expected_contents = """\ Simple Module ************* diff --git a/tox.ini b/tox.ini index 6777c399..77358498 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,7 @@ commands = description = format the code base to adhere to our styles, and complain about what we cannot do automatically skip_install = true deps = - pre-commit>=3.2.2 + pre-commit>=3.3.2 commands = pre-commit run --all-files --show-diff-on-failure @@ -50,20 +50,20 @@ set_env = [testenv:type] description = run type check on code base deps = - mypy==1.2 - types-docutils>=0.19.1.7 + mypy==1.3 + types-docutils>=0.20.0.1 set_env = {tty:MYPY_FORCE_COLOR = 1} commands = - mypy --python-version 3.10 src - mypy --python-version 3.10 tests + mypy src + mypy tests [testenv:coverage] description = combine coverage files and generate diff (against DIFF_AGAINST defaulting to origin/main) skip_install = true deps = covdefaults>=2.3 - coverage>=7.2.3 + coverage>=7.2.7 diff-cover>=7.5 extras = parallel_show_output = true @@ -78,6 +78,7 @@ commands = coverage html -d {toxworkdir}/htmlcov diff-cover --compare-branch {env:DIFF_AGAINST:origin/main} {toxworkdir}/coverage.xml depends = + py312 py311 py310 py39 @@ -101,13 +102,3 @@ package = editable commands = python -m pip list --format=columns python -c 'import sys; print(sys.executable)' - -[flake8] -max-complexity = 22 -max-line-length = 120 -ignore = E203, W503 -unused-arguments-ignore-abstract-functions = true -noqa-require-code = true - -[pep8] -max-line-length = 120