Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,14 @@ packages = [ "sphinxlint" ]
[tool.hatch.version.raw-options]
local_scheme = "no-local-version"

[tool.black]

[tool.ruff]
fix = true

[tool.isort]
profile = "black"

format.preview = true
lint.select = [
"E", # pycodestyle errors
Expand Down
7 changes: 6 additions & 1 deletion sphinxlint/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ def job_count(values):
"Values <= 1 are all considered 1.",
default=StoreNumJobsAction.job_count("auto"),
)
parser.add_argument(
"--check-docstrings",
action="store_true",
help="Also check docstrings in Python files.",
)
parser.add_argument(
"-V", "--version", action="version", version=f"%(prog)s {__version__}"
)
Expand Down Expand Up @@ -228,7 +233,7 @@ def main(argv=None):
return 2

todo = [
(path, enabled_checkers, options)
(path, enabled_checkers, options, args.check_docstrings)
for path in chain.from_iterable(walk(path, args.ignore) for path in args.paths)
]

Expand Down
18 changes: 14 additions & 4 deletions sphinxlint/sphinxlint.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from collections import Counter
from dataclasses import dataclass
from dataclasses import dataclass, replace
from os.path import splitext

from sphinxlint.utils import PER_FILE_CACHES, hide_non_rst_blocks, po2rst
from sphinxlint.utils import PER_FILE_CACHES, hide_non_rst_blocks, po2rst, py2rst


@dataclass(frozen=True)
Expand Down Expand Up @@ -49,7 +49,9 @@ def check_text(filename, text, checkers, options=None):
return errors


def check_file(filename, checkers, options: CheckersOptions = None):
def check_file(
filename, checkers, options: CheckersOptions | None = None, check_docstrings=False
):
try:
ext = splitext(filename)[1]
if not any(ext in checker.suffixes for checker in checkers):
Expand All @@ -63,7 +65,15 @@ def check_file(filename, checkers, options: CheckersOptions = None):
return [f"{filename}: cannot open: {err}"]
except UnicodeDecodeError as err:
return [f"{filename}: cannot decode as UTF-8: {err}"]
return check_text(filename, text, checkers, options)
errors = check_text(filename, text, checkers, options)
if check_docstrings and filename.endswith(".py"):
errors += [
replace(error, filename=error.filename[:-4])
for error in check_text(
filename + ".rst", py2rst(text), checkers, options
)
]
return errors
finally:
for memoized_function in PER_FILE_CACHES:
memoized_function.cache_clear()
20 changes: 20 additions & 0 deletions sphinxlint/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Just a bunch of utility functions for sphinxlint."""

import ast
from functools import lru_cache

import regex as re
Expand Down Expand Up @@ -230,3 +231,22 @@ def po2rst(text):
for line in entry.msgstr.splitlines():
output.append(line + "\n")
return "".join(output)


def py2rst(text):
output = []
tree = ast.parse(text)
for node in ast.walk(tree):
try:
docstring = ast.get_docstring(node, clean=True)
except TypeError:
continue
if docstring is None:
continue
if not hasattr(node, "lineno"):
node.lineno = 1
while len(output) + 1 <= node.lineno:
output.append("\n")
for line in docstring.splitlines():
output.append(line + "\n")
return "".join(output)
6 changes: 6 additions & 0 deletions tests/fixtures/xfail/docstring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# expect: default role used

"""This is a module docstring.

Containing BAD rst, like a `default` role.
"""
16 changes: 16 additions & 0 deletions tests/fixtures/xpass/class-docstring-containing-valid-rst.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class Youpi:
"""Dielectric profile calculation
==============================

In the following example, we will show how to calculate the
dielectric profiles as described in
:ref:`dielectric-explanations`.

Before producing trajectories to calculate dielectric profiles,
you will need to consider which information you will need and thus
need to print out. The dielectric profile calculators need
unwrapped positions and charges of **all** charged atoms in the
system. Unwrapped refers to the fact that you will need either
"repaired" molecules (which in GROMACS ``trjconv`` with the ``-pbc
mol`` option can do for you) or you will
"""
8 changes: 8 additions & 0 deletions tests/fixtures/xpass/docstrings-containing-plain-text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class TestPlanarBaseChilds:
"""Tests for the AnalayseBase child classes."""

ignored_parameters = ["atomgroup", "wrap_compound"]

def test_parameters(self):
"""Test if AnalysisBase paramaters exist in all modules."""
pass
17 changes: 17 additions & 0 deletions tests/test_py2rst.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from sphinxlint.utils import py2rst


def test_py2rst():
py = '''#!/usr/bin/env python
"""Hello from the module docstring!!!"""

def foo():
"""Hello from a function docstring!!!"""
'''
rst = """
Hello from the module docstring!!!


Hello from a function docstring!!!
"""
assert py2rst(py) == rst
37 changes: 30 additions & 7 deletions tests/test_sphinxlint.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import re
from pathlib import Path

import pytest

from sphinxlint.cli import main
from sphinxlint.utils import paragraphs

Expand All @@ -10,7 +10,9 @@

@pytest.mark.parametrize("file", [str(f) for f in (FIXTURE_DIR / "xpass").iterdir()])
def test_sphinxlint_shall_pass(file, capsys):
has_errors = main(["sphinxlint.py", "--enable", "all", str(file)])
has_errors = main(
["sphinxlint.py", "--check-docstrings", "--enable", "all", str(file)]
)
out, err = capsys.readouterr()
assert err == ""
assert out == "No problems found.\n"
Expand All @@ -26,7 +28,9 @@ def test_sphinxlint_shall_trigger_false_positive(file, capsys):
assert out == "No problems found.\n"
assert err == ""
assert not has_errors
has_errors = main(["sphinxlint.py", "--enable", "all", str(file)])
has_errors = main(
["sphinxlint.py", "--check-docstrings", "--enable", "all", str(file)]
)
out, err = capsys.readouterr()
assert out != "No problems found.\n"
assert err != ""
Expand All @@ -39,18 +43,18 @@ def gather_xfail():
Each file is searched for lines containing expcted errors, they
are starting with `.. expect: `.
"""
marker = ".. expect: "
marker = re.compile(r"(\.\.|#) expect: ")
for file in (FIXTURE_DIR / "xfail").iterdir():
expected_errors = []
for line in Path(file).read_text(encoding="UTF-8").splitlines():
if line.startswith(marker):
expected_errors.append(line[len(marker) :])
if match := marker.match(line):
expected_errors.append(line[len(match[0]) :])
yield str(file), expected_errors


@pytest.mark.parametrize("file,expected_errors", gather_xfail())
def test_sphinxlint_shall_not_pass(file, expected_errors, capsys):
has_errors = main(["sphinxlint.py", "--enable", "all", file])
has_errors = main(["sphinxlint.py", "--check-docstrings", "--enable", "all", file])
out, err = capsys.readouterr()
assert out != "No problems found.\n"
assert err != ""
Expand All @@ -68,6 +72,25 @@ def test_sphinxlint_shall_not_pass(file, expected_errors, capsys):
), f"{number_of_reported_errors=}, {err=}"


def test_check_docstrings_flag(subtests):
for file in (FIXTURE_DIR / "xfail").glob("*.py"):
with subtests.test(msg="with file", file=file):
with subtests.test(msg="With --check-docstrings"):
has_errors = main(
[
"sphinxlint.py",
"--check-docstrings",
"--enable",
"all",
str(file),
]
)
assert has_errors
with subtests.test(msg="Without --check-docstrings"):
has_errors = main(["sphinxlint.py", "--enable", "all", str(file)])
assert not has_errors


@pytest.mark.parametrize("file", [str(FIXTURE_DIR / "paragraphs.rst")])
def test_paragraphs(file):
with open(file) as f:
Expand Down
Loading