Skip to main content

rstwrap

Tests PyPI version Python versions Status License

A command-line tool to wrap prose paragraphs in reStructuredText (.rst) files to a maximum line width.

Only prose paragraphs and list items are wrapped. Everything else (directives, literal blocks, tables, section underlines, comments, indented blocks) is left unchanged.

- This is a very long paragraph that goes way beyond the standard seventy-nine characters and really should be wrapped for better readability in a terminal or text editor.
+ This is a very long paragraph that goes way beyond the standard seventy-nine
+ characters and really should be wrapped for better readability in a terminal or
+ text editor.

Primary workflows:

  • Local: format .rst files automatically on save in your editor.
  • CI: enforce consistent line width using the --check flag.

Installation

pip install rstwrap

Usage

Examples:

rstwrap docs/*.rst
rstwrap docs/                # whole dir, recursive
rstwrap --check docs/*.rst
rstwrap --width 120 foo.rst
rstwrap --no-join docs/*.rst  # only wrap over-width lines
rstwrap --safe docs/*.rst    # verify output with docutils
cat foo.rst | rstwrap -      # read stdin, write to stdout

Options:

  • -w, --width: maximum line length (default: 79)
  • --diff: print a unified diff instead of writing files
  • --color: colorize diff output (auto, always, never; default: auto)
  • --check: exit with code 1 if any file would be changed; don't write
  • --join / --no-join: merge short consecutive lines within a paragraph into a single line (up to the target width).
  • --safe: after wrapping, parse both the input and the output with docutils, and skip any file whose document tree would change (prints a diff to stderr and exits with code 1). Requires pip install 'rstwrap[safe]'.
  • -q, --quiet: suppress informational output.
  • --version: print the version and exit

Editor integration

Use - instead of a file path to read from stdin and write to stdout. This lets you integrate it into any editor that can pipe the current buffer through a shell command, and format .rst files on save.

Vim / Neovim

Add to ~/.vimrc:

autocmd BufWritePre *.rst silent! %!rstwrap -

VS Code

With the Custom Local Formatters extension:

"customLocalFormatters.formatters": [
  {
    "command": "rstwrap -",
    "languages": ["restructuredtext"]
  }
]

Sublime Text

With the Fmt plugin, add to Preferences > Package Settings > Fmt > Settings:

{
  "rules": [
    {
      "selector": "text.restructuredtext",
      "cmd": ["rstwrap", "-"],
      "format_on_save": true
    }
  ]
}

Emacs

(defun rstwrap-buffer ()
  (interactive)
  (let ((p (point)))
    (shell-command-on-region (point-min) (point-max)
                             "rstwrap -" nil t)
    (goto-char p)))

GitHub Actions

Add the following workflow into .github/workflows/rstwrap.yml to fail CI for any .rst file that isn't properly wrapped. Adjust docs/ to wherever your .rst files live.

name: rstwrap
on: [push, pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v6
        with:
          python-version: '3.x'
      - run: pip install 'rstwrap[safe]'
      - run: rstwrap --check --diff --safe docs/

Configuration via pyproject.toml

Project-wide defaults can be set in a [tool.rstwrap] section of pyproject.toml. The tool walks up from the current working directory to find the nearest one. Supported keys:

[tool.rstwrap]
width = 120    # default: 79
join = false   # default: true
safe = true    # default: false

Command-line flags override anything set in pyproject.toml. To turn off a setting from the CLI for a single run, use the negation flags --no-join / --no-safe. Per-invocation flags (--check, --diff) are not configurable in pyproject.toml — they're run modes, not project policy.

What gets wrapped

  • Prose paragraphs

    - This is a very long paragraph that goes way beyond the standard seventy-nine characters and really should be wrapped.
    + This is a very long paragraph that goes way beyond the standard
    + seventy-nine characters and really should be wrapped.
    
  • Lists (bullet and enumerated), including nested sublists

    - - This is a very long bullet item that exceeds the target width and needs to be re-wrapped to fit within the line limit.
    + - This is a very long bullet item that exceeds the target width and
    +   needs to be re-wrapped to fit within the line limit.
    
  • Bodies of directives that contain prose (.. note::, .. warning::, .. versionadded::, .. class::, etc.)

      .. note::
    -    This is a very long note that exceeds the target width and needs to be re-wrapped to fit within the line limit.
    +    This is a very long note that exceeds the target width and needs
    +    to be re-wrapped to fit within the line limit.
    
  • Short consecutive lines within a paragraph (disable with --no-join)

    - Some short
    - lines that
    - could fit on one.
    + Some short lines that could fit on one.
    

What gets formatted

Beyond wrapping, the tool also applies these normalizations everywhere (including lines that already fit within the target width):

  • Double or more spaces in prose are collapsed

    - hello  world
    + hello world
    
  • Consecutive blank lines between top-level paragraphs are collapsed into one. Blank lines inside indented content (literal blocks, directive bodies, simple tables) are preserved verbatim.

    - Paragraph one.
    -
    -
    -
    - Paragraph two.
    + Paragraph one.
    +
    + Paragraph two.
    
  • Trailing whitespace is stripped from every line

    - Some text with trailing spaces.···
    + Some text with trailing spaces.
    
  • \r\n (Windows line endings) are converted to \n (UNIX)

What is left untouched

  • Code blocks (.. code-block::, :: blocks)
  • Tables (grid and simple)
  • Section titles and underlines
  • Comments, hyperlink targets, substitution definitions
  • Field lists (:param foo:, :type bar:)
  • Option list items (-x, --foo)
  • Block quotes
  • Inline RST constructs (:role:`display <target>` , *emphasis*, **bold**, etc.) are treated as atomic tokens: they are never split across lines, and their internal whitespace is preserved.

Tested against real-world docs

This tool targets a very specific niche: formatting reStructuredText (RST) without breaking the semantic structure of the document. The integration test suite runs against a large corpus of real-world .rst files (~7800 in total) from several upstream projects:

For every file the suite verifies:

  • Idempotency: running the tool twice produces the same output as running it once.
  • Width: no tool-produced line exceeds the target width (verbatim passthrough of already-long source lines is allowed).
  • No double spaces: no tool-produced prose line contains a bare double space.
  • Document tree invariant: parsing the original and the wrapped file with docutils produces identical document trees (after normalising intra-node whitespace). This confirms that wrapping prose never alters headings, directives, code blocks, hyperlinks, or any other structural element.

License

MIT

Release files for rstwrap 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for rstwrap 0.1.1
File Size Uploaded
rstwrap-0.1.1.tar.gz 50.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for rstwrap 0.1.1
File Interpreter ABI Platform
rstwrap-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 69.1 kB

Release files / rstwrap-0.1.1.tar.gz

Download URL rstwrap-0.1.1.tar.gz
Size 50.9 kB
Tags Source
SHA-256 checksum
How to use checksums
122fbb7b161806f3fc87ff2ab53ff89ddca5044424bea0a90c2374153737a6cc
BLAKE2b-256 checksum
How to use checksums
eb3ad6b73bded79d5022038271fc0130cf28fb6f5935d39db0963b8ca8f29e82
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Apr 17, 2026.

Transparency log

Release files / rstwrap-0.1.1-py3-none-any.whl

Download URL rstwrap-0.1.1-py3-none-any.whl
Size 18.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
45552c3d8fd29892c5daee32f1482119a0cad085117f4495d19ff908cfe9d51b
BLAKE2b-256 checksum
How to use checksums
eae1b93a9e96a7e4b76312408638badb832cb60bcc751ba0b73e1f5b8b10a860
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Apr 17, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page