Skip to main content

Wrap prose paragraphs in reStructuredText files to a max line width

Project description

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rstwrap-0.1.1.tar.gz (50.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rstwrap-0.1.1-py3-none-any.whl (18.2 kB view details)

Uploaded Python 3

File details

Details for the file rstwrap-0.1.1.tar.gz.

File metadata

  • Download URL: rstwrap-0.1.1.tar.gz
  • Upload date:
  • Size: 50.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for rstwrap-0.1.1.tar.gz
Algorithm Hash digest
SHA256 122fbb7b161806f3fc87ff2ab53ff89ddca5044424bea0a90c2374153737a6cc
MD5 36a4ed8628842922fe7013c680afee71
BLAKE2b-256 eb3ad6b73bded79d5022038271fc0130cf28fb6f5935d39db0963b8ca8f29e82

See more details on using hashes here.

Provenance

The following attestation bundles were made for rstwrap-0.1.1.tar.gz:

Publisher: publish.yml on giampaolo/rstwrap

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rstwrap-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: rstwrap-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 18.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for rstwrap-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 45552c3d8fd29892c5daee32f1482119a0cad085117f4495d19ff908cfe9d51b
MD5 48ddfeeaad37039a2a1d6bfd4f961edc
BLAKE2b-256 eae1b93a9e96a7e4b76312408638badb832cb60bcc751ba0b73e1f5b8b10a860

See more details on using hashes here.

Provenance

The following attestation bundles were made for rstwrap-0.1.1-py3-none-any.whl:

Publisher: publish.yml on giampaolo/rstwrap

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page