diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e7e4be5..b0d6c33 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -13,12 +13,12 @@ concurrency: jobs: test: name: test on CPython ${{ matrix.py }} - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest strategy: fail-fast: false matrix: py: - - "3.12.0-beta.1" + - "3.12.0-beta.2" - "3.11" - "3.10" - 3.9 @@ -54,7 +54,7 @@ jobs: check: name: tox env ${{ matrix.tox_env }} - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest strategy: fail-fast: false matrix: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 19cf635..8046620 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ on: jobs: release: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest environment: name: release url: https://pypi.org/p/sphinx-argparse-cli diff --git a/.markdownlint.yaml b/.markdownlint.yaml deleted file mode 100644 index f30749b..0000000 --- a/.markdownlint.yaml +++ /dev/null @@ -1,8 +0,0 @@ -MD013: - code_blocks: false - headers: false - line_length: 120 - tables: false - -MD046: - style: fenced diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d96f526..1732483 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,78 +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/add-trailing-comma - rev: v2.4.0 - hooks: - - id: add-trailing-comma - args: [--py36-plus] - - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 - hooks: - - id: pyupgrade - args: ["--py38-plus"] - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort - 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.12.0" 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 - - flake8-pyproject==1.2.3 + - id: pyproject-fmt + additional_dependencies: ["tox>=4.6.1"] - 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 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.0.272" hooks: - - id: markdownlint + - id: ruff + args: [--fix, --exit-non-zero-on-fix] - 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/README.md b/README.md index a143801..ac43178 100644 --- a/README.md +++ b/README.md @@ -72,13 +72,13 @@ to avoid reference label clash when the same anchors are generated in multiple d For example in case of a `tox` command, and `sphinx_argparse_cli_prefix_document=False` (default): -- to refer to the optional arguments group use `` :ref:`tox-optional-arguments` ``, -- to refer to the run subcommand use `` :ref:`tox-run` ``, -- to refer to flag `--magic` of the `run` sub-command use `` :ref:`tox-run---magic` ``. +- to refer to the optional arguments group use ``:ref:`tox-optional-arguments` ``, +- to refer to the run subcommand use ``:ref:`tox-run` ``, +- to refer to flag `--magic` of the `run` sub-command use ``:ref:`tox-run---magic` ``. For example in case of a `tox` command, and `sphinx_argparse_cli_prefix_document=True`, and the current document name being `cli`: -- to refer to the optional arguments group use `` :ref:`cli:tox-optional-arguments` ``, -- to refer to the run subcommand use `` :ref:`cli:tox-run` ``, -- to refer to flag `--magic` of the `run` sub-command use `` :ref:`cli:tox-run---magic` ``. +- to refer to the optional arguments group use ``:ref:`cli:tox-optional-arguments` ``, +- to refer to the run subcommand use ``:ref:`cli:tox-run` ``, +- to refer to flag `--magic` of the `run` sub-command use ``:ref:`cli:tox-run---magic` ``. diff --git a/pyproject.toml b/pyproject.toml index 78af4d2..5fea7b4 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.18", ] [project] @@ -14,7 +14,7 @@ keywords = [ "sphinx", ] license = "MIT" -maintainers = [{ name = "Bernat Gabor", email = "gaborjbernat@gmail.com" }] +maintainers = [{ name = "Bernat Gabor", email = "gaborjbernat@gmail.com" }] # noqa: E999 requires-python = ">=3.8" classifiers = [ "Development Status :: 5 - Production/Stable", @@ -24,8 +24,12 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", - "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", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Documentation", "Topic :: Documentation :: Sphinx", @@ -34,12 +38,12 @@ dynamic = [ "version", ] dependencies = [ - "sphinx>=6.1.3", + "sphinx>=7.0.1", ] optional-dependencies.test = [ "covdefaults>=2.3", - "pytest>=7.3.1", - "pytest-cov>=4", + "pytest>=7.3.2", + "pytest-cov>=4.1", ] urls.Documentation = "https://github.com/tox-dev/sphinx-argparse-cli#sphinx-argparse-cli" urls.Homepage = "https://github.com/tox-dev/sphinx-argparse-cli" @@ -54,20 +58,30 @@ version.source = "vcs" [tool.black] line-length = 120 -[tool.isort] -known_first_party = ["sphinx_argparse_cli", "tests"] -profile = "black" -line_length = 120 - -[tool.flake8] -max-complexity = 22 -max-line-length = 120 -unused-arguments-ignore-abstract-functions = true -noqa-require-code = true -dictionaries = ["en_US", "python", "technical", "django"] +[tool.ruff] +select = ["ALL"] +line-length = 120 +target-version = "py38" +isort = {known-first-party = ["sphinx_argparse_cli"], required-imports = ["from __future__ import annotations"]} ignore = [ - "E203", # whitespace before ':' - "W503", # line break before binary operator + "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 +] +"roots/**/*.py" = [ + "INP001", # no namespace + "D", # no docs ] [tool.coverage] @@ -84,6 +98,3 @@ run.relative_files = true python_version = "3.11" show_error_codes = true strict = true - -[tool.pep8] -max-line-length = "120" diff --git a/roots/test-default-handling/parser.py b/roots/test-default-handling/parser.py index fa3973a..91c54e7 100644 --- a/roots/test-default-handling/parser.py +++ b/roots/test-default-handling/parser.py @@ -7,4 +7,4 @@ def main() -> None: parser = ArgumentParser(prog="foo", add_help=False) parser.add_argument("x", default=1, help="arg (default: True)") args = parser.parse_args() - print(args) + print(args) # noqa: T201 diff --git a/roots/test-hook-fail/parser.py b/roots/test-hook-fail/parser.py index 2c9c6cc..e76cc6e 100644 --- a/roots/test-hook-fail/parser.py +++ b/roots/test-hook-fail/parser.py @@ -5,4 +5,4 @@ def main() -> None: parser = ArgumentParser(prog="foo", add_help=False) - print(parser) + print(parser) # noqa: T201 diff --git a/roots/test-hook/parser.py b/roots/test-hook/parser.py index 3d8b2b5..4534723 100644 --- a/roots/test-hook/parser.py +++ b/roots/test-hook/parser.py @@ -6,4 +6,4 @@ def main() -> None: parser = ArgumentParser(prog="foo", add_help=False) args = parser.parse_args() - print(args) + print(args) # noqa: T201 diff --git a/src/sphinx_argparse_cli/__init__.py b/src/sphinx_argparse_cli/__init__.py index 513ea86..edf08b0 100644 --- a/src/sphinx_argparse_cli/__init__.py +++ b/src/sphinx_argparse_cli/__init__.py @@ -1,9 +1,13 @@ +"""Sphinx generator for argparse.""" from __future__ import annotations -from sphinx.application import Sphinx +from typing import TYPE_CHECKING from .version import __version__ +if TYPE_CHECKING: + from sphinx.application import Sphinx + def setup(app: Sphinx) -> None: app.add_css_file("custom.css") @@ -11,7 +15,9 @@ def setup(app: Sphinx) -> None: from ._logic import SphinxArgparseCli app.add_directive(SphinxArgparseCli.name, SphinxArgparseCli) - app.add_config_value("sphinx_argparse_cli_prefix_document", False, "env") + app.add_config_value("sphinx_argparse_cli_prefix_document", False, "env") # noqa: FBT003 -__all__ = ("__version__",) +__all__ = [ + "__version__", +] diff --git a/src/sphinx_argparse_cli/_logic.py b/src/sphinx_argparse_cli/_logic.py index 6dc25f9..e0c4dbd 100644 --- a/src/sphinx_argparse_cli/_logic.py +++ b/src/sphinx_argparse_cli/_logic.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os import re import sys from argparse import ( @@ -14,7 +13,8 @@ _SubParsersAction, ) from collections import defaultdict, namedtuple -from typing import Any, Iterator, cast +from pathlib import Path +from typing import TYPE_CHECKING, Any, Iterator, cast from docutils.nodes import ( Element, @@ -33,13 +33,15 @@ whitespace_normalize_name, ) from docutils.parsers.rst.directives import flag, positive_int, unchanged, unchanged_required -from docutils.parsers.rst.states import RSTState, RSTStateMachine from docutils.statemachine import StringList from sphinx.domains.std import StandardDomain from sphinx.locale import __ from sphinx.util.docutils import SphinxDirective from sphinx.util.logging import getLogger +if TYPE_CHECKING: + from docutils.parsers.rst.states import RSTState, RSTStateMachine + TextAsDefault = namedtuple("TextAsDefault", ["text"]) @@ -66,7 +68,7 @@ class SphinxArgparseCli(SphinxDirective): "no_default_values": unchanged, } - def __init__( + def __init__( # noqa: PLR0913 self, name: str, arguments: list[str], @@ -77,7 +79,7 @@ def __init__( block_text: str, state: RSTState, state_machine: RSTStateMachine, - ): + ) -> None: options.setdefault("group_title_prefix", None) options.setdefault("group_sub_title_prefix", None) super().__init__(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine) @@ -91,32 +93,34 @@ def parser(self) -> ArgumentParser: parser_creator = getattr(__import__(module_name, fromlist=[attr_name]), attr_name) if "hook" in self.options: original_parse_known_args = ArgumentParser.parse_known_args - ArgumentParser.parse_known_args = _argparse_parse_known_args_hook # type: ignore + ArgumentParser.parse_known_args = _parse_known_args_hook # type: ignore[method-assign,assignment] try: parser_creator() except HookError as hooked: self._parser = hooked.parser finally: - ArgumentParser.parse_known_args = original_parse_known_args # type: ignore + ArgumentParser.parse_known_args = original_parse_known_args # type: ignore[method-assign] else: self._parser = parser_creator() del sys.modules[module_name] # no longer needed cleanup if self._parser is None: - raise self.error("Failed to hook argparse to get ArgumentParser") + msg = "Failed to hook argparse to get ArgumentParser" + raise self.error(msg) if "prog" in self.options: self._parser.prog = self.options["prog"] return self._parser def load_sub_parsers(self) -> Iterator[tuple[list[str], str, ArgumentParser]]: - top_sub_parser = self.parser._subparsers + top_sub_parser = self.parser._subparsers # noqa: SLF001 if not top_sub_parser: return parser_to_args: dict[int, list[str]] = defaultdict(list) str_to_parser: dict[str, ArgumentParser] = {} - sub_parser: _SubParsersAction[ArgumentParser] = top_sub_parser._group_actions[0] # type: ignore - for key, parser in sub_parser._name_parser_map.items(): + sub_parser: _SubParsersAction[ArgumentParser] + sub_parser = top_sub_parser._group_actions[0] # type: ignore[assignment] # noqa: SLF001 + for key, parser in sub_parser._name_parser_map.items(): # noqa: SLF001 parser_to_args[id(parser)].append(key) str_to_parser[key] = parser done_parser: set[int] = set() @@ -128,14 +132,14 @@ def load_sub_parsers(self) -> Iterator[tuple[list[str], str, ArgumentParser]]: aliases = parser_to_args[id(parser)] aliases.remove(name) # help is stored in a pseudo action - help_msg = next((a.help for a in sub_parser._choices_actions if a.dest == name), None) or "" + help_msg = next((a.help for a in sub_parser._choices_actions if a.dest == name), None) or "" # noqa: SLF001 yield aliases, help_msg, parser def run(self) -> list[Node]: # construct headers self.env.note_reread() # this document needs to be always updated title_text = self.options.get("title", f"{self.parser.prog} - CLI interface").strip() - if title_text.strip() == "": + if not title_text.strip(): home_section: Element = paragraph() else: home_section = section("", title("", Text(title_text)), ids=[make_id(title_text)], names=[title_text]) @@ -146,8 +150,8 @@ def run(self) -> list[Node]: home_section += desc_paragraph # construct groups excluding sub-parsers home_section += self._mk_usage(self.parser) - for group in self.parser._action_groups: - if not group._group_actions or group is self.parser._subparsers: + for group in self.parser._action_groups: # noqa: SLF001 + if not group._group_actions or group is self.parser._subparsers: # noqa: SLF001 continue home_section += self._mk_option_group(group, prefix=self.parser.prog.split("/")[-1]) # construct sub-parser @@ -169,7 +173,7 @@ def _mk_option_group(self, group: _ArgumentGroup, prefix: str) -> section: group_section += paragraph("", Text(group.description)) self._register_ref(ref_id, title_text, group_section) opt_group = bullet_list() - for action in group._group_actions: + for action in group._group_actions: # noqa: SLF001 point = self._mk_option_line(action, prefix) opt_group += point group_section += opt_group @@ -186,15 +190,14 @@ def _build_opt_grp_title(self, group: _ArgumentGroup, prefix: str, sub_title_pre title_text = self._append_title(title_text, sub_title_prefix, elements[0], " ".join(elements[1:])) else: title_text += f"{' '.join(prefix.split(' ')[1:])} " - else: - if " " in prefix: - if sub_title_prefix is not None: - title_text += f"{elements[0]} " - title_text = self._append_title(title_text, sub_title_prefix, elements[0], " ".join(elements[1:])) - else: - title_text += f"{' '.join(elements[:2])} " + elif " " in prefix: + if sub_title_prefix is not None: + title_text += f"{elements[0]} " + title_text = self._append_title(title_text, sub_title_prefix, elements[0], " ".join(elements[1:])) else: - title_text += f"{prefix} " + title_text += f"{' '.join(elements[:2])} " + else: + title_text += f"{prefix} " title_text += group.title or "" return title_text @@ -233,7 +236,7 @@ def _mk_option_line(self, action: Action, prefix: str) -> list_item: and not isinstance(action, (_StoreTrueAction, _StoreFalseAction)) ): line += Text(" (default: ") - line += literal(text=str(action.default).replace(os.getcwd(), "{cwd}")) + line += literal(text=str(action.default).replace(str(Path.cwd()), "{cwd}")) line += Text(")") return point @@ -248,7 +251,13 @@ def _mk_option_name(self, line: paragraph, prefix: str, opt: str) -> None: self._register_ref(ref_id, ref_title, ref, is_cli_option=True) line += ref - def _register_ref(self, ref_name: str, ref_title: str, node: Element, is_cli_option: bool = False) -> None: + def _register_ref( + self, + ref_name: str, + ref_title: str, + node: Element, + is_cli_option: bool = False, # noqa: FBT001, FBT002 + ) -> None: doc_name = self.env.docname normalize_name = whitespace_normalize_name if is_cli_option else fully_normalize_name if self.env.config.sphinx_argparse_cli_prefix_document: @@ -287,8 +296,8 @@ def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentPar desc_paragraph = paragraph("", Text(command_desc)) group_section += desc_paragraph group_section += self._mk_usage(parser) - for group in parser._action_groups: - if not group._group_actions: # do not show empty groups + for group in parser._action_groups: # noqa: SLF001 + if not group._group_actions: # do not show empty groups # noqa: SLF001 continue group_section += self._mk_option_group(group, prefix=parser.prog) return group_section @@ -303,12 +312,11 @@ def _build_sub_cmd_title(self, parser: ArgumentParser, sub_title_prefix: str, ti title_text = self._append_title(title_text, sub_title_prefix, elements[0], elements[1]) else: title_text += elements[1] + elif sub_title_prefix is not None: + title_text += f"{elements[0]} " + title_text = self._append_title(title_text, sub_title_prefix, elements[0], elements[1]) else: - if sub_title_prefix is not None: - title_text += f"{elements[0]} " - title_text = self._append_title(title_text, sub_title_prefix, elements[0], elements[1]) - else: - title_text += parser.prog + title_text += parser.prog return title_text.rstrip() @staticmethod @@ -334,17 +342,18 @@ def _mk_usage(self, parser: ArgumentParser) -> literal_block: def load_help_text(help_text: str) -> str: single_quote = SINGLE_QUOTE.sub("``'\\1'``", help_text) double_quote = DOUBLE_QUOTE.sub('``"\\1"``', single_quote) - literal_curly_braces = CURLY_BRACES.sub("``{\\1}``", double_quote) - return literal_curly_braces + return CURLY_BRACES.sub("``{\\1}``", double_quote) class HookError(Exception): - def __init__(self, parser: ArgumentParser): + def __init__(self, parser: ArgumentParser) -> None: self.parser = parser -def _argparse_parse_known_args_hook(self: ArgumentParser, *args: Any, **kwargs: Any) -> None: # noqa: U100 +def _parse_known_args_hook(self: ArgumentParser, *args: Any, **kwargs: Any) -> None: # noqa: ARG001 raise HookError(self) -__all__ = ("SphinxArgparseCli",) +__all__ = [ + "SphinxArgparseCli", +] diff --git a/tests/conftest.py b/tests/conftest.py index 5f51ed5..03ba96c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,16 +1,20 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest -from _pytest.config import Config from docutils import __version__ as docutils_version from sphinx import __display_version__ as sphinx_version from sphinx.testing.path import path +if TYPE_CHECKING: + from _pytest.config import Config + pytest_plugins = "sphinx.testing.fixtures" collect_ignore = ["roots"] -def pytest_report_header(config: Config) -> str: # noqa: U100 +def pytest_report_header(config: Config) -> str: # noqa: ARG001 return f"libraries: Sphinx-{sphinx_version}, docutils-{docutils_version}" diff --git a/tests/test_logic.py b/tests/test_logic.py index 7a219fc..d405e08 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -2,12 +2,16 @@ import os import sys -from io import StringIO from pathlib import Path +from typing import TYPE_CHECKING import pytest -from _pytest.fixtures import SubRequest -from sphinx.testing.util import SphinxTestApp + +if TYPE_CHECKING: + from io import StringIO + + from _pytest.fixtures import SubRequest + from sphinx.testing.util import SphinxTestApp @pytest.fixture(scope="session") @@ -29,7 +33,7 @@ def build_outcome(app: SphinxTestApp, request: SubRequest) -> str: if not any(i for i in directive_args if i.startswith(":func:")): # pragma: no branch directive_args.append(":func: make") args = [f" {i}" for i in directive_args] - index.write_text(os.linesep.join([".. sphinx_argparse_cli::"] + args)) + index.write_text(os.linesep.join([".. sphinx_argparse_cli::", *args])) ext_mapping = {"html": "html", "text": "txt"} sphinx_marker = request.node.get_closest_marker("sphinx") @@ -37,8 +41,7 @@ def build_outcome(app: SphinxTestApp, request: SubRequest) -> str: ext = ext_mapping[sphinx_marker.kwargs.get("buildername")] app.build() - text = (Path(app.outdir) / f"index.{ext}").read_text() - return text + return (Path(app.outdir) / f"index.{ext}").read_text() @pytest.mark.sphinx(buildername="html", testroot="basic") @@ -68,7 +71,7 @@ def test_hook_fail(app: SphinxTestApp, warning: StringIO) -> None: app.build() text = (Path(app.outdir) / "index.txt").read_text() assert "Failed to hook argparse to get ArgumentParser" in warning.getvalue() - assert text == "" + assert not text @pytest.mark.sphinx(buildername="text", testroot="prog") diff --git a/tox.ini b/tox.ini index 3ef5d4f..e93e64d 100644 --- a/tox.ini +++ b/tox.ini @@ -36,7 +36,7 @@ description = run static analysis and style check using flake8 base_python = python3.10 skip_install = true deps = - pre-commit>=3.2.2 + pre-commit>=3.3.3 pass_env = HOMEPATH PROGRAMDATA @@ -47,8 +47,8 @@ commands = [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 = diff --git a/whitelist.txt b/whitelist.txt deleted file mode 100644 index 397537d..0000000 --- a/whitelist.txt +++ /dev/null @@ -1,18 +0,0 @@ -addinivalue -anonlabels -autosectionlabel -buildername -confdir -doc2path -docname -docutils -fromlist -grp -nitpicky -outdir -refid -reftitle -rst -statemachine -subparsers -testroot