Skip to main content

octowrap

CI PyPI Python License

Rewrap Python # comments to a line length you choose without touching commented-out code, section dividers, TODO/FIXME markers, or tool directives.

Animated terminal demo of `octowrap -i` reviewing three comment changes in a small Python file: a long prose comment, a TODO marker, and an overflowing inline comment. Each diff appears in red/green and is accepted with a single keystroke.

Features

  • Rewraps comment blocks to a configurable line length (default 88)
  • Keeps hyphenated words intact (never breaks command-line-interface at hyphens)
  • Keeps long words and URLs intact (they overflow the line length rather than being broken mid-word)
  • Heals previously broken hyphenated words on rewrap (e.g. re- / validate -> re-validate)
  • Heals erroneous spaces at bracket boundaries on rewrap (e.g. ( text) -> (text), text ) -> text))
  • Preserves commented-out Python code (detected via 21 heuristic patterns with a prose disqualifier to avoid false positives on natural English)
  • Preserves section dividers (# --------, # ========, etc.)
  • Preserves section headers (# === Title ===, # --- Title ---, # ### Title ###, # *** Title ***, # ___ Title ___): same delimiter character on both sides, three or more per side, asymmetric counts allowed
  • Rewraps list items (bullets, numbered, lettered) with hanging indent aligned to the text after the marker; collects continuation lines and handles nesting naturally. Disable with list-wrap = false.
  • Rewraps TODO/FIXME markers with proper continuation indent, with configurable patterns, case sensitivity, and multi-line collection
  • Extracts overflowing inline comments (code # comment) into standalone block comments above the code line when the line exceeds the line length, then wraps them normally. Tool directives (# type: ignore, # noqa, etc.) are always preserved in place. Disable with --no-inline.
  • Preserves tool directives (type: ignore, noqa, fmt: off, pragma: no cover, pylint: disable, noinspection, etc.)
  • Supports # octowrap: off / # octowrap: on pragma comments to disable rewrapping for regions of a file
  • Applies changes automatically by default, or use -i for interactive per-paragraph approval with colorized diffs and a [X/Y] progress indicator (a accept, A accept all remaining paragraphs in the file, e exclude, f flag, s skip, u undo, q quit). A single comment block that mixes prose, a TODO, and a tool directive reviews as separate diffs so you can see exactly what's changing. Consecutive list items group into a single prompt. Flagging wraps the paragraph with a FIXME marker and # octowrap: off / # octowrap: on pragmas so reruns skip it. Undo pops the most recent decision and re-prompts at that position; it works across files (a previously-written file is reverted on disk lazily at quit or on the next walk-through). Quitting stops all processing, including remaining files; on quit, every file on disk is reconciled with the final decision log so undone writes are reverted.
  • Reads from stdin when - is passed as the path (like black/ruff/isort)
  • Auto-detects color support; respects --no-color, --color, and the NO_COLOR env var
  • Atomic file writes (temp file + rename) to protect against interruptions and power loss
  • Incremental adoption via --diff-only: only process comments on lines changed in git, so teams can adopt octowrap gradually without reformatting the entire codebase
  • Project-level configuration via [tool.octowrap] in pyproject.toml

Development Setup

git clone https://github.com/camUrban/octowrap.git
cd octowrap
uv sync            # uses .python-version (3.14) and installs runtime + dev group

Note: The dev environment is pinned to Python 3.14 via .python-version. The runtime itself supports 3.11+.

Usage

octowrap <files_or_dirs> [--line-length 88] [--config PATH] [--stdin-filename PATH] [--dry-run] [--diff] [--check] [--no-recursive] [--no-inline] [--diff-only] [--diff-base REF] [-i] [--color | --no-color]

Stdin/stdout

Pass - as the path to read from stdin and write to stdout:

echo "# A very long comment that needs rewrapping to a shorter width." | octowrap -
cat file.py | octowrap - --diff          # show diff
cat file.py | octowrap - --check         # exit 1 if changes needed
cat file.py | octowrap - -l 79           # custom line length

Use --stdin-filename to provide the original file path for config discovery and diff labels (useful for editor integrations like VS Code and Vim that pipe buffers via stdin):

cat file.py | octowrap - --stdin-filename src/app.py --diff

Note: - cannot be mixed with other paths and is incompatible with -i (interactive mode). --stdin-filename requires - and must end in .py.

Heads up: in bare stdin mode (octowrap - without --stdin-filename), octowrap has no way to tell what kind of content is being piped in and will rewrap it as Python comments regardless. Unlike formatters that parse to an AST, octowrap operates lexically on #-prefixed lines, so it will happily produce plausible-looking but wrong output from a Markdown file. The caller is responsible for ensuring only Python source flows in. Editor integrations should gate on filetype, and shell pipelines should filter for .py before piping.

Example

Before:

# This is a long comment that has been written without much regard for line length and really should be wrapped to fit within a reasonable number of columns.

After (--line-length 88):

# This is a long comment that has been written without much regard for line
# length and really should be wrapped to fit within a reasonable number of
# columns.

Inline Comment Extraction

When a code line with an inline comment exceeds the line length, octowrap extracts the comment into a standalone block comment above the code:

Before:

x = some_really_long_function_call(arg1, arg2)  # This comment pushes the line way past the limit

After (--line-length 88):

# This comment pushes the line way past the limit
x = some_really_long_function_call(arg1, arg2)

Tool directives (# type: ignore, # noqa, # fmt: off, etc.) are never extracted, even when the line overflows. Disable this behavior entirely with --no-inline or inline = false in config.

TODO/FIXME Rewrapping

By default, TODO and FIXME markers are detected (case-insensitive, no colon required) and rewrapped with the marker on the first line and a one-space continuation indent on subsequent lines:

Before:

# TODO: Refactor this function to use the new async API instead of the old synchronous one, and update all callers.

After (--line-length 88):

# TODO: Refactor this function to use the new async API instead of the old
#  synchronous one, and update all callers.

Multi-line TODOs (continuation lines starting with exactly one space) are collected and rewrapped together:

# TODO: This is a long todo
#  that continues on the next line

Configure TODO handling via pyproject.toml:

[tool.octowrap]
todo-patterns = ["todo", "fixme", "hack"]    # replace default patterns
extend-todo-patterns = ["note"]              # add to effective patterns
todo-case-sensitive = true                   # match patterns literally
todo-multiline = false                       # don't collect continuations

Setting todo-patterns = [] disables TODO detection entirely, causing former TODO lines to be rewrapped as regular prose.

List Item Wrapping

Long list items are rewrapped with hanging indent aligned to the text after the marker:

Before:

# - This is a very long bullet point that exceeds the line length and should be wrapped to fit within the configured width.
# 1. This is a very long numbered item that also exceeds the line length and needs to be wrapped properly.

After (--line-length 72):

# - This is a very long bullet point that exceeds the line length and
#   should be wrapped to fit within the configured width.
# 1. This is a very long numbered item that also exceeds the line length
#    and needs to be wrapped properly.

Nesting is handled naturally. Each item wraps independently at its own indent level:

# - Top-level item
#   - Nested item that is quite long and will be wrapped with its own
#     hanging indent aligned to the nested marker

Continuation lines indented to at least the marker's text column are collected and rewrapped together. Disable with list-wrap = false in pyproject.toml.

Disabling Rewrapping

Use pragma comments to protect regions of a file from rewrapping, similar to # fmt: off/on in black/ruff:

# octowrap: off
# This comment will not be rewrapped,
# no matter how long or short
# the lines are.
# octowrap: on

# This comment will be rewrapped normally.
  • Directives are case-insensitive (# OCTOWRAP: OFF works)
  • Must be a standalone comment line (inline x = 1 # octowrap: off is ignored)
  • # octowrap: off without a matching on disables rewrapping through end of file
  • Pragma lines themselves are always preserved as-is

Pre-commit Hook

The recommended setup is the octowrap-check hook, which fails without modifying files so you can review each rewrap interactively before it lands. Add it to your .pre-commit-config.yaml:

- repo: https://github.com/camUrban/octowrap
  rev: v0.7.1
  hooks:
    - id: octowrap-check
      # args: [-l, "79"]       # custom line length
      # args: [--diff-only]    # only check comments on changed lines

Run pre-commit autoupdate --freeze to replace the tag with its commit SHA plus a # frozen: v0.7.1 comment, the form Dependabot keeps current and the one that protects against a moved tag.

When octowrap-check fails, octowrap prints the commands to fix the reported files, built from the interpreter that ran the check. Under pre-commit that is the copy in pre-commit's cached hook environment, so the interactive fix runs against the exact pinned rev without installing octowrap separately:

Run: /home/you/.cache/pre-commit/repoXXXX/py_env-python3.14/bin/python -m octowrap -i src/a.py

No TTY:
  Review: /home/you/.cache/pre-commit/repoXXXX/py_env-python3.14/bin/python -m octowrap --diff src/a.py

  Apply: /home/you/.cache/pre-commit/repoXXXX/py_env-python3.14/bin/python -m octowrap src/a.py

The interactive Run command needs a terminal. The No TTY pair reviews and then applies the same changes without prompting, for agents and other non-interactive callers. If the interpreter path contains whitespace it is double-quoted and each label notes that PowerShell users must prefix the command with &.

Alternatively, the octowrap hook rewraps comments in place like a formatter, with no review step. It takes the same args:

- repo: https://github.com/camUrban/octowrap
  rev: v0.7.1
  hooks:
    - id: octowrap

Continuous Integration

Run the octowrap-check hook through pre-commit in CI so the check uses the exact rev pinned in .pre-commit-config.yaml:

- name: Install pre-commit
  run: pip install pre-commit

- name: Check comment wrapping
  run: pre-commit run --all-files octowrap-check

Without pre-commit, install octowrap directly and run it with --check:

- name: Install octowrap
  run: pip install octowrap

- name: Check comment wrapping
  run: octowrap --check .

To only enforce wrapping on comments changed in the PR, add --diff-only --diff-base origin/main (as hook args, or as flags in the direct form). A full git history is required so that origin/main is available for comparison, so check out with fetch-depth: 0.

Exit Codes

Code Meaning
0 Success (no changes needed, or changes applied)
1 --check mode: files would be reformatted
2 Error processing one or more files (e.g., encoding error, permission denied)

Errors are printed to stderr. This behavior matches ruff.

Incremental Adoption

Use --diff-only to only process comment blocks that overlap with lines changed in git. This lets teams adopt octowrap gradually without reformatting the entire codebase in one go:

# Only rewrap comments on lines you've changed vs HEAD
octowrap --diff-only .

# Only rewrap comments changed relative to main
octowrap --diff-only --diff-base main --check .

# Preview what would change
octowrap --diff-only --diff .

--diff-base REF specifies the git ref to diff against (default: HEAD). Passing --diff-base implies --diff-only.

Comment blocks are processed at the block level: if any line in a comment block overlaps with a changed line, the entire block is rewrapped. This is safe because comment blocks are syntactically independent.

Both diff-only and diff-base can also be set in pyproject.toml:

[tool.octowrap]
diff-only = true
diff-base = "main"

In pre-commit and CI, pass --diff-only through the hook's args as shown in Pre-commit Hook and Continuous Integration.

Note: --diff-only requires a git repository and cannot be used with stdin mode (-).

Editor Integration

PyCharm

Settings -> Tools -> File Watchers -> Add:

  • File type: Python
  • Program: $ProjectFileDir$/.venv/Scripts/octowrap.exe (or .venv/bin/octowrap on Unix)
  • Arguments: $FilePath$
  • Output paths to refresh: $FilePath$
  • Working directory: $ProjectFileDir$

Configuration

Add a [tool.octowrap] section to your pyproject.toml to set project-level defaults:

[tool.octowrap]
line-length = 120
recursive = false
inline = true
exclude = ["migrations", "generated"]
extend-exclude = ["vendor"]
Key Type Default CLI equivalent
line-length int 88 --line-length
recursive bool true --no-recursive
inline bool true --no-inline
list-wrap bool true n/a
diff-only bool false --diff-only
diff-base str "HEAD" --diff-base
exclude list[str] (built-in list) n/a
extend-exclude list[str] [] n/a
todo-patterns list[str] ["todo", "fixme"] n/a
extend-todo-patterns list[str] [] n/a
todo-case-sensitive bool false n/a
todo-multiline bool true n/a

CLI flags always take precedence over config values. Use --config PATH to point to a specific pyproject.toml instead of relying on auto-discovery.

exclude replaces the built-in default exclude list entirely. extend-exclude adds patterns to the defaults (or to exclude if set). Default excludes: .git, .hg, .svn, .bzr, .venv, venv, .tox, .nox, .mypy_cache, .ruff_cache, .pytest_cache, __pycache__, __pypackages__, _build, build, dist, node_modules, .eggs. Patterns are matched against individual folder or file names using fnmatch, not full paths. For example, "vendor" excludes any folder named vendor anywhere in the tree, while "docs/vendor" would never match (use "vendor" instead). Glob wildcards work: "test_*" excludes any folder starting with test_.

todo-patterns replaces the default TODO marker patterns (["todo", "fixme"]). extend-todo-patterns adds to the effective list. Both can be combined. Setting todo-patterns = [] disables TODO detection entirely.

License

MIT

Metadata

Release files for octowrap 0.7.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 octowrap 0.7.1
File Size Uploaded
octowrap-0.7.1.tar.gz 88.4 kB Details

Built distribution (wheel)

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

Total release size: 122.5 kB

Release files / octowrap-0.7.1.tar.gz

Download URL octowrap-0.7.1.tar.gz
Size 88.4 kB
Tags Source
SHA-256 checksum
How to use checksums
e9aaaa49d820c8c3afa6d392019cf56183e5b319f988e27d5dfc6dca0c3efe8d
BLAKE2b-256 checksum
How to use checksums
d7d0434c54d8f9ecce317f20663ad4a54a429a5dfdcd8f709727e243dc059690
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 16, 2026.

Transparency log

Release files / octowrap-0.7.1-py3-none-any.whl

Download URL octowrap-0.7.1-py3-none-any.whl
Size 34.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e81224a7f6eb54b1aa5dae96fa425e4ba232ee28e9da5405a568ea9b849e426a
BLAKE2b-256 checksum
How to use checksums
ba138dcad0ffae8b71ebda9242af296bb433473a65dbbab5306bb48cb3077d56
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.7.1 This release

2 release files

0.7.0

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.2.0

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