diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..850976317 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,20 @@ +name: Lint + +on: [push, pull_request, workflow_dispatch] + +env: + FORCE_COLOR: 1 + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ff94f3e2b..4592194cc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - # when adding new versions, update the one used to test + # when adding new versions, update the one used to test # friend projects below to the latest one python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] os: [ubuntu-latest, macos-latest, windows-latest] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..7447b5ce2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,48 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.6 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.11.0 + hooks: + - id: black + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-case-conflict + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + exclude: tests/fixtures/xfail/missing-newline-at-end-of-file.rst + - id: trailing-whitespace + exclude: tests/fixtures/xfail/trailing-whitespaces.rst + + - repo: https://github.com/tox-dev/pyproject-fmt + rev: 1.5.1 + hooks: + - id: pyproject-fmt + additional_dependencies: [tox] + + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.15 + hooks: + - id: validate-pyproject + + - repo: https://github.com/tox-dev/tox-ini-fmt + rev: 1.3.1 + hooks: + - id: tox-ini-fmt + + - repo: meta + hooks: + - id: check-hooks-apply + - id: check-useless-excludes + +ci: + autoupdate_schedule: quarterly diff --git a/README.md b/README.md index afb5200ee..39541db9c 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ $ docutils --writer=pseudoxml tests/fixtures/xpass/role-in-code-sample.rst 1. Make sure that the [CI tests pass](https://github.com/sphinx-contrib/sphinx-lint/actions) and optionally double-check locally with "friends projects" by running: - + sh download-more-tests.sh python -m pytest 2. Go on the [Releases page](https://github.com/sphinx-contrib/sphinx-lint/releases) diff --git a/pyproject.toml b/pyproject.toml index 298c32435..69bb7c8cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,31 +14,36 @@ authors = [ {name = "Georg Brandl", email = "georg@python.org"}, {name = "Julien Palard", email = "julien@palard.fr"}, ] +requires-python = ">= 3.8" classifiers = [ - "Development Status :: 5 - Production/Stable", - "Topic :: Documentation :: Sphinx", - "Intended Audience :: Developers", - "License :: OSI Approved :: Python Software Foundation License", - "Natural Language :: English", - "Programming Language :: Python :: 3", + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Python Software Foundation License", + "Natural Language :: English", + "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 :: 3.13", + "Topic :: Documentation :: Sphinx", +] +dynamic = [ + "version", ] -requires-python = ">= 3.8" dependencies = [ - "regex", - "polib", + "polib", + "regex", ] -dynamic = ["version"] - - [project.optional-dependencies] tests = [ "pytest", "pytest-cov", ] [project.urls] -Repository = "https://github.com/sphinx-contrib/sphinx-lint" Changelog = "https://github.com/sphinx-contrib/sphinx-lint/releases" - +Repository = "https://github.com/sphinx-contrib/sphinx-lint" [project.scripts] sphinx-lint = "sphinxlint.cli:main" @@ -50,5 +55,25 @@ local_scheme = "no-local-version" [tool.black] +[tool.ruff] +select = [ + "E", # pycodestyle errors + "F", # pyflakes errors + "I", # isort + "ISC", # flake8-implicit-str-concat + "PGH", # pygrep-hooks + "RUF100", # unused noqa (yesqa) + "UP", # pyupgrade + "W", # pycodestyle warnings + "YTT", # flake8-2020 +] +extend-ignore = [ + "E203", # Whitespace before ':' + "E221", # Multiple spaces before operator + "E226", # Missing whitespace around arithmetic operator + "E241", # Multiple spaces after ',' + "UP038", # makes code slower and more verbose +] + [tool.pylint.variables] callbacks = ["check_"] diff --git a/sphinxlint/checkers.py b/sphinxlint/checkers.py index 791cac6ca..141e3a73d 100644 --- a/sphinxlint/checkers.py +++ b/sphinxlint/checkers.py @@ -12,7 +12,6 @@ paragraphs, ) - all_checkers = {} @@ -60,7 +59,10 @@ def check_missing_backtick_after_role(file, lines, options=None): error = rst.ROLE_MISSING_CLOSING_BACKTICK_RE.search(paragraph) if error: error_offset = paragraph[: error.start()].count("\n") - yield paragraph_lno + error_offset, f"role missing closing backtick: {error.group(0)!r}" + yield ( + paragraph_lno + error_offset, + f"role missing closing backtick: {error.group(0)!r}", + ) _RST_ROLE_RE = re.compile("``.+?``(?!`).", flags=re.DOTALL) @@ -129,8 +131,12 @@ def check_default_role(file, lines, options=None): before_match = line[: match.start()] after_match = line[match.end() :] stripped_line = line.strip() - if (stripped_line.startswith("|") and stripped_line.endswith("|") and - stripped_line.count("|") >= 4 and "|" in match.group(0)): + if ( + stripped_line.startswith("|") + and stripped_line.endswith("|") + and stripped_line.count("|") >= 4 + and "|" in match.group(0) + ): return # we don't handle tables yet. if _ends_with_role_tag(before_match): # It's not a default role: it ends with a tag. @@ -141,7 +147,10 @@ def check_default_role(file, lines, options=None): if match.group(0).startswith("``") and match.group(0).endswith("``"): # It's not a default role: it's an inline literal. continue - yield lno, "default role used (hint: for inline literals, use double backticks)" + yield ( + lno, + "default role used (hint: for inline literals, use double backticks)", + ) @checker(".rst", ".po") @@ -287,7 +296,10 @@ def check_role_with_double_backticks(file, lines, options=None): before = paragraph[: inline_literal.start()] if _ends_with_role_tag(before): error_offset = paragraph[: inline_literal.start()].count("\n") - yield paragraph_lno + error_offset, "role use a single backtick, double backtick found." + yield ( + paragraph_lno + error_offset, + "role use a single backtick, double backtick found.", + ) paragraph = ( paragraph[: inline_literal.start()] + paragraph[inline_literal.end() :] ) @@ -308,9 +320,15 @@ def check_missing_space_before_role(file, lines, options=None): if match: error_offset = paragraph[: match.start()].count("\n") if looks_like_glued(match): - yield paragraph_lno + error_offset, f"missing space before role ({match.group(0)})." + yield ( + paragraph_lno + error_offset, + f"missing space before role ({match.group(0)}).", + ) else: - yield paragraph_lno + error_offset, f"role missing opening tag colon ({match.group(0)})." + yield ( + paragraph_lno + error_offset, + f"role missing opening tag colon ({match.group(0)}).", + ) @checker(".rst", ".po") @@ -494,4 +512,4 @@ def check_dangling_hyphen(file, lines, options): for lno, line in enumerate(lines): stripped_line = line.rstrip("\n") if _has_dangling_hyphen(stripped_line): - yield lno + 1, f"Line ends with dangling hyphen" + yield lno + 1, "Line ends with dangling hyphen" diff --git a/sphinxlint/cli.py b/sphinxlint/cli.py index 55d2bdaf5..c0a5638bf 100644 --- a/sphinxlint/cli.py +++ b/sphinxlint/cli.py @@ -54,7 +54,8 @@ def __call__(self, parser, namespace, values, option_string=None): sort_fields.append(SortField[field_name.upper()]) except KeyError: raise ValueError( - f"Unsupported sort field: {field_name}, supported values are {SortField.as_supported_options()}" + f"Unsupported sort field: {field_name}, " + f"supported values are {SortField.as_supported_options()}" ) from None setattr(namespace, self.dest, sort_fields) @@ -85,7 +86,8 @@ def job_count(values): "-d", "--disable", action=DisableAction, - help='comma-separated list of checks to disable. Give "all" to disable them all. ' + help="comma-separated list of checks to disable. " + 'Give "all" to disable them all. ' "Can be used in conjunction with --enable (it's evaluated left-to-right). " '"--disable all --enable trailing-whitespace" can be used to enable a ' "single check.", diff --git a/sphinxlint/utils.py b/sphinxlint/utils.py index 1dfb3ffec..6aee9d51a 100644 --- a/sphinxlint/utils.py +++ b/sphinxlint/utils.py @@ -6,7 +6,6 @@ from sphinxlint import rst - PER_FILE_CACHES = [] @@ -187,10 +186,7 @@ def hide_non_rst_blocks(lines, hidden_block_cb=None): in_literal = len(_ZERO_OR_MORE_SPACES_RE.match(line)[0]) block_line_start = lineno assert not excluded_lines - if ( - type_of_explicit_markup(line) == "comment" - and _COMMENT_RE.search(line) - ): + if type_of_explicit_markup(line) == "comment" and _COMMENT_RE.search(line): line = "\n" output.append(line) if excluded_lines and hidden_block_cb: diff --git a/tests/test_default_role_re.py b/tests/test_default_role_re.py index 86dd2d23a..5e68eaa4b 100644 --- a/tests/test_default_role_re.py +++ b/tests/test_default_role_re.py @@ -12,5 +12,5 @@ def test_shall_not_pass(): assert not rst.INTERPRETED_TEXT_RE.search("``") assert not rst.INTERPRETED_TEXT_RE.search("2 * x a ** b (* BOM32_* ` `` _ __ |") assert not rst.INTERPRETED_TEXT_RE.search( - """"`" '|' (`) [`] {`} <`> ‘`’ ‚`‘ ‘`‚ ’`’ ‚`’ “`” „`“ “`„ ”`” „`” »`« ›`‹ «`» »`» ›`›""" + """"`" '|' (`) [`] {`} <`> ‘`’ ‚`‘ ‘`‚ ’`’ ‚`’ “`” „`“ “`„ ”`” „`” »`« ›`‹ «`» »`» ›`›""" # noqa: E501 ) diff --git a/tests/test_enable_disable.py b/tests/test_enable_disable.py index 1d0c1fd45..1fd506da9 100644 --- a/tests/test_enable_disable.py +++ b/tests/test_enable_disable.py @@ -1,5 +1,5 @@ -from random import choice import re +from random import choice from sphinxlint.cli import main diff --git a/tests/test_filter_out_literal.py b/tests/test_filter_out_literal.py index 4ea7f54a5..34a7792dd 100644 --- a/tests/test_filter_out_literal.py +++ b/tests/test_filter_out_literal.py @@ -1,6 +1,5 @@ from sphinxlint.utils import hide_non_rst_blocks - LITERAL = r""" Hide non-RST Blocks =================== diff --git a/tests/test_sphinxlint.py b/tests/test_sphinxlint.py index 85b08afaa..4e68ed5a1 100644 --- a/tests/test_sphinxlint.py +++ b/tests/test_sphinxlint.py @@ -1,10 +1,9 @@ from pathlib import Path -from sphinxlint.utils import paragraphs - import pytest from sphinxlint.cli import main +from sphinxlint.utils import paragraphs FIXTURE_DIR = Path(__file__).resolve().parent / "fixtures" @@ -64,7 +63,9 @@ def test_sphinxlint_shall_not_pass(file, expected_errors, capsys): assert expected_error in err number_of_expected_errors = len(expected_errors) number_of_reported_errors = len(err.splitlines()) - assert number_of_expected_errors == number_of_reported_errors, f"{number_of_reported_errors=}, {err=}" + assert ( + number_of_expected_errors == number_of_reported_errors + ), f"{number_of_reported_errors=}, {err=}" @pytest.mark.parametrize("file", [str(FIXTURE_DIR / "paragraphs.rst")]) diff --git a/tests/test_xpass_friends.py b/tests/test_xpass_friends.py index 206b35068..2bb42aa0a 100644 --- a/tests/test_xpass_friends.py +++ b/tests/test_xpass_friends.py @@ -3,14 +3,13 @@ This is useful to avoid a sphinx-lint release to break many CIs. """ -from pathlib import Path import shlex +from pathlib import Path import pytest from sphinxlint.cli import main - FIXTURE_DIR = Path(__file__).resolve().parent / "fixtures" diff --git a/tox.ini b/tox.ini index fe5261b02..e616fc791 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ requires = tox>=4.2 env_list = + lint py{py3, 313, 312, 311, 310, 39, 38} [testenv] @@ -17,3 +18,12 @@ commands = --cov-report term \ --cov-report xml \ {posargs} + +[testenv:lint] +skip_install = true +deps = + pre-commit +pass_env = + PRE_COMMIT_COLOR +commands = + pre-commit run --all-files --show-diff-on-failure