Created
January 28, 2026 23:59
-
-
Save stephenmathieson/9b0faa1d4018675cfccc5c908ea8d818 to your computer and use it in GitHub Desktop.
Automatically wrap long comments in python files (because `black` does not)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """wrap_comments.py. | |
| A script to auto-wrap full-line comments in Python files to a specified line length (default 88). | |
| - Only wraps lines starting with '#' (not inline comments or code). | |
| - Preserves indentation and comment markers. | |
| - Skips docstrings and code. | |
| Usage: | |
| python wrap_comments.py <file_or_directory> [--line-length N] [--dry-run] | |
| """ | |
| import argparse | |
| import os | |
| import textwrap | |
| from pathlib import Path | |
| from typing import List | |
| def wrap_comment_line(line: str, line_length: int) -> List[str]: | |
| stripped = line.lstrip() | |
| indent = line[: len(line) - len(stripped)] | |
| # Only wrap if this is a single-line Python comment (not markdown heading) | |
| # e.g. '# comment', not '##' or '###' etc. | |
| if not (stripped.startswith("#") and (len(stripped) == 1 or stripped[1] == " ")): | |
| return [line.rstrip("\n")] | |
| # Remove leading '#' and possible space | |
| comment_body = stripped[1:] | |
| if comment_body.startswith(" "): | |
| comment_body = comment_body[1:] | |
| # Wrap the comment | |
| wrapped = textwrap.wrap(comment_body, width=line_length - len(indent) - 2) | |
| if not wrapped: | |
| return [indent + "#"] | |
| return [indent + "# " + w for w in wrapped] | |
| def process_file(path: Path, line_length: int, dry_run: bool = False) -> int: | |
| changed = False | |
| with path.open("r", encoding="utf-8") as f: | |
| lines = f.readlines() | |
| new_lines = [] | |
| for line in lines: | |
| if line.lstrip().startswith("#") and not line.lstrip().startswith("#!"): | |
| wrapped = wrap_comment_line(line, line_length) | |
| if wrapped != [line.rstrip("\n")]: | |
| changed = True | |
| new_lines.extend(wrapped) | |
| else: | |
| new_lines.append(line.rstrip("\n")) | |
| if changed and not dry_run: | |
| with path.open("w", encoding="utf-8") as f: | |
| for l in new_lines: | |
| f.write(l + "\n") | |
| return int(changed) | |
| def process_path(target: Path, line_length: int, dry_run: bool = False) -> int: | |
| changed_files = 0 | |
| if target.is_file() and target.suffix == ".py": | |
| changed_files += process_file(target, line_length, dry_run) | |
| elif target.is_dir(): | |
| for root, _, files in os.walk(target): | |
| for fname in files: | |
| # Only process .py files | |
| if fname.endswith(".py"): | |
| changed_files += process_file( | |
| Path(root) / fname, line_length, dry_run | |
| ) | |
| return changed_files | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Wrap full-line comments in Python files." | |
| ) | |
| parser.add_argument("target", help="File or directory to process") | |
| parser.add_argument( | |
| "--line-length", type=int, default=88, help="Max line length (default 88)" | |
| ) | |
| parser.add_argument( | |
| "--dry-run", | |
| action="store_true", | |
| help="Show what would change, don't write files", | |
| ) | |
| args = parser.parse_args() | |
| target = Path(args.target) | |
| changed = process_path(target, args.line_length, args.dry_run) | |
| if args.dry_run: | |
| print(f"[DRY RUN] {changed} file(s) would be changed.") | |
| else: | |
| print(f"{changed} file(s) updated.") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment