diff --git a/sphinxlint/__main__.py b/sphinxlint/__main__.py index 181f7872c..5e879dd32 100644 --- a/sphinxlint/__main__.py +++ b/sphinxlint/__main__.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) @@ -152,8 +153,8 @@ def _check_file(todo): def sort_errors(results, sorted_by): """Flattens and potentially sorts errors based on user prefernces""" if not sorted_by: - for results in results: - yield from results + for result in results: + yield from result return errors = list(error for errors in results for error in errors) # sorting is stable in python, so we can sort in reverse order to get the diff --git a/sphinxlint/checkers.py b/sphinxlint/checkers.py index c310323a3..d80ca4af2 100644 --- a/sphinxlint/checkers.py +++ b/sphinxlint/checkers.py @@ -55,7 +55,7 @@ def check_missing_backtick_after_role(file, lines, options=None): Good: :fct:`foo` """ for paragraph_lno, paragraph in paragraphs(lines): - if paragraph.count("|") > 4: + if rst.paragraph_looks_like_a_table(paragraph): return # we don't handle tables yet. error = rst.ROLE_MISSING_CLOSING_BACKTICK_RE.search(paragraph) if error: @@ -71,7 +71,7 @@ def check_missing_space_after_literal(file, lines, options=None): Good: ``items``\ s """ for paragraph_lno, paragraph in paragraphs(lines): - if paragraph.count("|") > 4: + if rst.paragraph_looks_like_a_table(paragraph): return # we don't handle tables yet. paragraph = clean_paragraph(paragraph) for role in re.finditer("``.+?``(?!`).", paragraph, flags=re.DOTALL): @@ -92,7 +92,7 @@ def check_unbalanced_inline_literals_delimiters(file, lines, options=None): Good: ``hello`` world """ for paragraph_lno, paragraph in paragraphs(lines): - if paragraph.count("|") > 4: + if rst.paragraph_looks_like_a_table(paragraph): return # we don't handle tables yet. paragraph = clean_paragraph(paragraph) for lone_double_backtick in re.finditer("(?= 4 and "|" in match.group(0)): + if rst.line_looks_like_a_table(line): return # we don't handle tables yet. if re.search(rst.ROLE_TAG + "$", before_match): # It's not a default role: it starts with a tag. @@ -253,7 +251,7 @@ def check_role_with_double_backticks(file, lines, options=None): for paragraph_lno, paragraph in paragraphs(lines): if "`" not in paragraph: continue - if paragraph.count("|") > 4: + if rst.paragraph_looks_like_a_table(paragraph): return # we don't handle tables yet. paragraph = escape2null(paragraph) while True: @@ -267,7 +265,10 @@ def check_role_with_double_backticks(file, lines, options=None): before = paragraph[: inline_literal.start()] if re.search(rst.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() :] ) @@ -281,16 +282,22 @@ def check_missing_space_before_role(file, lines, options=None): Good: the :fct:`sum`, :issue:`123`, :c:func:`foo` """ for paragraph_lno, paragraph in paragraphs(lines): - if paragraph.count("|") > 4: + if rst.paragraph_looks_like_a_table(paragraph): return # we don't handle tables yet. paragraph = clean_paragraph(paragraph) match = rst.ROLE_GLUED_WITH_WORD_RE.search(paragraph) 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") @@ -301,7 +308,7 @@ def check_missing_space_before_default_role(file, lines, options=None): Good: the `sum` """ for paragraph_lno, paragraph in paragraphs(lines): - if paragraph.count("|") > 4: + if rst.paragraph_looks_like_a_table(paragraph): return # we don't handle tables yet. paragraph = clean_paragraph(paragraph) paragraph = rst.INTERPRETED_TEXT_RE.sub("", paragraph) @@ -324,7 +331,7 @@ def check_hyperlink_reference_missing_backtick(file, lines, options=None): Good: `Misc/NEWS `_ """ for paragraph_lno, paragraph in paragraphs(lines): - if paragraph.count("|") > 4: + if rst.paragraph_looks_like_a_table(paragraph): return # we don't handle tables yet. paragraph = clean_paragraph(paragraph) paragraph = rst.INTERPRETED_TEXT_RE.sub("", paragraph) @@ -459,4 +466,4 @@ def check_dangling_hyphen(file, lines, options): for lno, line in enumerate(lines): stripped_line = line.rstrip("\n") if re.match(r".*[a-z]-$", stripped_line): - yield lno + 1, f"Line ends with dangling hyphen" + yield lno + 1, "Line ends with dangling hyphen" diff --git a/sphinxlint/rst.py b/sphinxlint/rst.py index a5e88351b..d9f55c50d 100644 --- a/sphinxlint/rst.py +++ b/sphinxlint/rst.py @@ -277,3 +277,19 @@ def inline_markup_gen(start_string, end_string, extra_allowed_before=""): ) ROLE_MISSING_CLOSING_BACKTICK_RE = re.compile(rf"({ROLE_HEAD}`[^`]+?)[^`]*$") + + +TABLE_HEAD_RE = re.compile(r"^\+[+=-]+\+") + + +def line_looks_like_a_table(line): + """Return true if the given line looks part of an rst table.""" + line = line.strip() + if TABLE_HEAD_RE.match(line): + return True + return line.startswith("|") and line.endswith("|") + + +def paragraph_looks_like_a_table(paragraph): + """Return true if the given paragraph looks like an rst table.""" + return all(line_looks_like_a_table(line) for line in paragraph.splitlines()) diff --git a/sphinxlint/sphinxlint.py b/sphinxlint/sphinxlint.py index d9b2e6a09..ec146cae2 100644 --- a/sphinxlint/sphinxlint.py +++ b/sphinxlint/sphinxlint.py @@ -1,6 +1,8 @@ from collections import Counter from dataclasses import dataclass from os.path import splitext +from pathlib import Path +from typing import Optional from sphinxlint.utils import hide_non_rst_blocks, po2rst @@ -49,13 +51,12 @@ def check_text(filename, text, checkers, options=None): return errors -def check_file(filename, checkers, options: CheckersOptions = None): +def check_file(filename, checkers, options: Optional[CheckersOptions] = None): ext = splitext(filename)[1] if not any(ext in checker.suffixes for checker in checkers): return Counter() try: - with open(filename, encoding="utf-8") as f: - text = f.read() + text = Path(filename).read_text(encoding="UTF-8") if filename.endswith(".po"): text = po2rst(text) except OSError as err: diff --git a/tests/fixtures/xfail/default-role-in-tables.rst b/tests/fixtures/xfail/default-role-in-tables.rst deleted file mode 100644 index d85631cdd..000000000 --- a/tests/fixtures/xfail/default-role-in-tables.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. expect: default role used (hint: for inline literals, use double backticks) (default-role) - -In the following table there are a couple of default roles that should fail: - -+-----------------+-------------------------------+----------------------------+ -| `French` | Julien Palard `@JulienPalard` | `GitHub `_ | -+-----------------+-------------------------------+----------------------------+ diff --git a/tests/test_enable_disable.py b/tests/test_enable_disable.py index fde5ae6ee..39436ba19 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.__main__ 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 5739b3e7b..2d3c52cf5 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.__main__ import main +from sphinxlint.utils import paragraphs FIXTURE_DIR = Path(__file__).resolve().parent / "fixtures" @@ -69,8 +68,8 @@ def test_sphinxlint_shall_not_pass(file, expected_errors, capsys): @pytest.mark.parametrize("file", [str(FIXTURE_DIR / "paragraphs.rst")]) def test_paragraphs(file): - with open(file) as f: - lines = f.readlines() + with open(file, encoding="UTF-8") as ifile: + lines = ifile.readlines() actual = paragraphs(lines) for lno, para in actual: firstpline = para.splitlines(keepends=True)[0] diff --git a/tests/test_xpass_friends.py b/tests/test_xpass_friends.py index c2953ae44..1faa20bcc 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.__main__ import main - FIXTURE_DIR = Path(__file__).resolve().parent / "fixtures" diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..547f86ce3 --- /dev/null +++ b/tox.ini @@ -0,0 +1,26 @@ +[tox] +envlist = py3{8,9,10,11,12}, mypy, black, pylint +isolated_build = True +skip_missing_interpreters = True + +[testenv] +deps = pytest +commands = pytest {posargs} + +[testenv:black] +deps = black +skip_install = True +commands = black --check --diff . + +[testenv:mypy] +deps = + mypy + types-polib +skip_install = True +commands = mypy --exclude fixtures --ignore-missing-imports sphinxlint/ tests/ + +[testenv:pylint] +deps = + pylint + pytest +commands = pylint --ignore fixtures sphinxlint/ tests/ --disable missing-function-docstring,missing-module-docstring,missing-class-docstring,too-few-public-methods,too-many-return-statements --good-names=po