Repository: robocorp/rpaframework

Issue:
Documentation: Implement `flake8` rst docstring linting
We should implement docstring linting to help catch reST formatting errors before building documentation. The following libraries could be useful in that regard:

* https://pypi.org/project/flake8-docstrings/
* https://pypi.org/project/flake8-rst-docstrings/



Discussion:

Resolving patch:
diff --git a/invocations/analysis.py b/invocations/analysis.py
--- a/invocations/analysis.py
+++ b/invocations/analysis.py
@@ -7,8 +7,13 @@
 
 from invoke import task, Collection
 
-from invocations import shell, config
-from invocations.util import REPO_ROOT, safely_load_config, remove_blank_lines
+from invocations import shell, config, docs
+from invocations.util import (
+    REPO_ROOT,
+    MAIN_PACKAGE,
+    safely_load_config,
+    remove_blank_lines,
+)
 
 try:
     from colorama import Fore, Style
@@ -25,6 +30,8 @@
 ROBOT_TEST_OUTPUT = Path("tests/results")
 ROBOT_TEST_RESOURCES = Path("tests/resources")
 
+MAIN_README = MAIN_PACKAGE / "README.rst"
+
 EXCLUDE_ROBOT_TESTS = ["skip"]
 if platform.system() == "Windows":
     EXCLUDE_ROBOT_TESTS.append("posix")
@@ -32,22 +39,58 @@
     EXCLUDE_ROBOT_TESTS.append("windows")
 
 
-@task(config.install)
-def lint(ctx):
-    """Run format checks and static analysis"""
-    if getattr(ctx, "is_meta", False):
-        shell.invoke_each(ctx, "code.lint")
+@task(
+    config.install,
+    help={
+        "docstrings": "Also check docstring format.",
+        "all": "Run linting against all packages as well.",
+        "exit_on_failure": "Causes task to exit when checks fail. Used for publish and build.",
+    },
+)
+def lint(ctx, docstrings=False, all=False, exit_on_failure=False):
+    """Run format checks and static analysis. By default this task does
+    not exit when checks fail.
+
+    When ran at the meta package level, this task runs linters against
+    documentation and does a dummy build of the Sphinx docs. You can
+    also have it run the lint command for all packages by setting
+    ``--all``.
+    """
+    warn_setting = not exit_on_failure
+    flake8_config = Path(safely_load_config(ctx, "ctx.linters.flake8", FLAKE8_CONFIG))
+    pylint_config = Path(safely_load_config(ctx, "ctx.linters.pylint", PYLINT_CONFIG))
+    if docstrings:
+        ignore_codes_cmd = ""
+        all_docstrings_cmd = "--docstrings"
     else:
-        flake8_config = Path(
-            safely_load_config(ctx, "ctx.linters.flake8", FLAKE8_CONFIG)
+        ignore_codes_cmd = "--extend-ignore D,RST"
+        all_docstrings_cmd = ""
+    if getattr(ctx, "is_meta", False):
+        shell.poetry(
+            ctx,
+            f"run py -m sphinxlint {docs.DOCS_SOURCE_DIR} {MAIN_README}",
+            warn=warn_setting,
         )
-        pylint_config = Path(
-            safely_load_config(ctx, "ctx.linters.pylint", PYLINT_CONFIG)
+        shell.sphinx(
+            ctx,
+            f"-b dummy -a -n --keep-going {docs.DOCS_SOURCE_DIR} {docs.DOCS_BUILD_DIR}",
+            warn=warn_setting,
         )
-
-        shell.poetry(ctx, "run black --diff --check src")
-        shell.poetry(ctx, f"run flake8 --config {flake8_config} src")
-        shell.poetry(ctx, f"run pylint --rcfile {pylint_config} src")
+        shell.poetry(
+            ctx,
+            f"run flake8 --config {flake8_config} {docs.DOCS_SOURCE_DIR}",
+            warn=warn_setting,
+        )
+        if all:
+            shell.invoke_each(ctx, f"code.lint {all_docstrings_cmd}")
+    else:
+        shell.poetry(ctx, "run black --diff --check src", warn=warn_setting)
+        shell.poetry(
+            ctx,
+            f"run flake8 --config {flake8_config} {ignore_codes_cmd} src",
+            warn=warn_setting,
+        )
+        shell.poetry(ctx, f"run pylint --rcfile {pylint_config} src", warn=warn_setting)
 
 
 @task(config.install, aliases=["pretty"])
diff --git a/invocations/build.py b/invocations/build.py
--- a/invocations/build.py
+++ b/invocations/build.py
@@ -7,7 +7,7 @@
 from pathlib import Path
 from invoke import task, Collection, ParseError, Context, config as inv_config
 
-from invocations import shell, config, libspec, ROBOT_BUILD_STRATEGY, errors
+from invocations import shell, config, libspec, ROBOT_BUILD_STRATEGY
 from invocations.util import require_package, safely_load_config, REPO_ROOT
 
 
@@ -37,7 +37,7 @@ def build(ctx, test=True):
             safely_load_config(ctx, "build_strategy") == ROBOT_BUILD_STRATEGY
         )
         if test:
-            shell.invoke(ctx, "code.lint", echo=False)
+            shell.invoke(ctx, "code.lint -e", echo=False)
             shell.invoke(ctx, "code.test -a", echo=False)
         if is_robot_build:
             libspec.clean_libspec(ctx)


Test patch: