restructuredtext-lint
A Python library and CLI that lints reStructuredText for structural errors before PyPI or Sphinx ever see it.
Repository Health
Technical Analysis
restructuredtext-lint is a lightweight Python library and CLI tool that checks reStructuredText (RST) documents for structural errors before they cause problems downstream — most notably on PyPI, where invalid RST silently falls back to plain-text rendering after upload. It wraps docutils’ publishing pipeline, attaching an error observer to the parser and transformer stages so every warning, error, and severe message docutils would normally print to stderr is instead captured as a structured object with line number, message, and severity.
The library exposes a simple lint(content) / lint_file(path) API that a build script, pre-commit hook, or third-party tool can call directly, and it also ships the rst-lint (aka restructuredtext-lint) command-line utility for standalone use, with text or JSON output and a configurable minimum severity level.
What You Get
- A
lint(content, filepath=None, rst_prolog=None)function returning structured error objects (line, source, level, type, message, full_message). - A
lint_file(filepath, *args, **kwargs)convenience wrapper for linting straight from disk with UTF-8 handling built in. - The
rst-lintCLI binary with--format text|jsonand--level debug|info|warning|error|severeflags for scripting and CI use. - Documentation and examples for extending the linter with custom docutils directives/roles (e.g. Sphinx-specific syntax like
highlight).
Common Use Cases
- Linting README.rst locally before running
twine uploadto PyPI. - Gating CI builds on
rst-lint --level warningacross a docs/ tree. - Powering editor/IDE inline RST linting plugins.
- Serving as the validation engine other RST or Sphinx-focused lint tools import rather than reimplement.
Under The Hood
Architecture
The package is essentially a single-purpose wrapper around docutils: restructuredtext_lint/__init__.py re-exports lint and lint_file from lint.py, which manually assembles a docutils Publisher (bypassing the higher-level publish_* helpers) so it can attach a custom document.reporter.attach_observer(error_collector) before parsing and transforming, capturing every system_message docutils emits as a structured object. cli.py is a thin argparse layer that walks file/directory paths for *.rst files, calls lint_file, filters by severity, and formats output as text or JSON with a CI-friendly exit-code contract (0 success, 1 internal error, 2 lint failure). The structure is intentionally flat — three source files plus a VERSION file and a test/ package — with both the library API and CLI funneling through the same lint_file/lint entry points, so changing the reporter-wiring core would ripple through both surfaces at once.
Tech Stack
Pure Python (97%+ of the codebase per GitHub’s language breakdown), with a single runtime dependency, docutils>=0.11,<1.0, and dev/test dependencies of nose-py3, flake8, and flake8-quotes. Packaging uses classic setuptools (setup.py + find_packages()), sourcing the version from a standalone VERSION file specifically to avoid importing the package during build. Releases are automated through a .foundryrc config driving foundry-release-git/foundry-release-pypi, with a documented custom-command workaround (python -m build + twine upload) for a known upstream wheel-publishing bug. CI runs on GitHub Actions via a test.sh script. There is no web framework, database, or external service beyond the docutils/Sphinx directive-registration hooks documented in the README.
Code Quality
Tests live in restructuredtext_lint/test/test.py using standard-library unittest.TestCase (run through nose-py3), with fixture .rst files under test/test_files/ covering valid RST, invalid RST, UTF-8 encoding, missing files, warning-level filtering, and directory-recursion behavior. Error handling in cli.py wraps lint_file calls in try/except to report which file was being linted before re-raising, and the CLI’s three distinct exit codes make failure modes unambiguous for scripting. The codebase predates type hints (no annotations, from __future__ import absolute_import still present), naming is consistent snake_case throughout, and flake8/flake8-quotes enforce style in CI.
API Design
The public surface is deliberately minimal — two functions, lint and lint_file, with no required configuration — returning the same docutils system_message objects docutils itself produces, augmented with exactly the attributes a caller needs (line, source, level, type, message, full_message) rather than a bespoke wrapper class, which keeps results interoperable with anything already working with docutils. The rst_prolog parameter is a thoughtful ergonomic touch: it lets callers prepend shared substitution definitions across lint calls while automatically correcting reported line numbers so errors still point at real content. The CLI mirrors that simplicity — one positional paths argument accepting files or directories, sensible defaults, and a documented exit-code contract that’s trivial to wire into CI. Extending behavior (e.g. registering Sphinx directives) leans entirely on docutils’ own APIs rather than anything this package provides itself, which keeps the surface small at the cost of a slightly steeper path for advanced customization.