Skip to main content

Format only the literal string arguments of textwrap.dedent() calls

Project description

format-dedent

Format multiline strings with proper indentation โ€” A Python code formatter that formats only the literal string arguments of textwrap.dedent() calls.

โœจ What does it do?

format-dedent automatically formats multiline strings inside textwrap.dedent() calls to make them visually match their runtime output. This makes your code more readable while preserving the exact behavior.

Key features:

  • ๐ŸŽฏ Surgical precision โ€” Only formats strings inside dedent() calls, leaves everything else untouched
  • ๐Ÿ”„ Two modes โ€” Format existing dedent strings OR automatically add dedent() to strings that need it
  • ๐Ÿ‘€ Safe โ€” Validates that formatting doesn't change runtime behavior
  • ๐ŸŽจ Smart indentation โ€” Aligns content with the visual structure of your code
  • ๐Ÿงน Clean โ€” Removes trailing whitespace and normalizes spacing

๐Ÿ“ฆ Installation

pip install format-dedent

๐Ÿš€ Quick Start

Format strings (default mode)

Preview formatted output without modifying files:

python -m format_dedent yourfile.py

Write changes to files:

python -m format_dedent yourfile.py --write

Format multiple files or directories:

python -m format_dedent src/ tests/ --write

Add dedent() calls (--add-dedent mode)

Automatically wrap multiline strings with dedent() calls:

python -m format_dedent yourfile.py --add-dedent --write

This will:

  • Find multiline strings where dedent(str) == str (no leading indentation to remove)
  • Wrap them with dedent() calls
  • Add from textwrap import dedent import if needed

๐Ÿ“– Usage Options

python -m format_dedent [OPTIONS] [FILES/DIRECTORIES]

Options:
  -w, --write       Write changes to files (default: output to stdout)
  --add-dedent      Add dedent() calls to multiline strings
  -h, --help        Show help message

Behavior:

  • Default โ†’ Output formatted code to stdout (no file modification)
  • --write โ†’ Modify files directly and print confirmation

Examples:

# Preview formatted code (output to stdout, no changes)
python -m format_dedent myfile.py

# Write changes to the file
python -m format_dedent myfile.py --write

# Format entire project
python -m format_dedent src/ tests/ --write

# Add dedent() to all multiline strings
python -m format_dedent myfile.py --add-dedent --write

# Read from stdin, write to stdout (pipe-friendly)
cat myfile.py | python -m format_dedent > formatted.py

๐Ÿ”ง Pre-commit Hook

Use format-dedent as a pre-commit hook to automatically format dedent strings before each commit.

Add this to your .pre-commit-config.yaml:

repos:
  - repo: https://github.com/15r10nk/format-dedent
    rev: v0.1.0  # Use the latest version
    hooks:
      - id: format-dedent

Then install the hook:

pre-commit install

Run manually on all files:

pre-commit run format-dedent --all-files

๐Ÿ’ก Examples

Example 1: Formatting SQL queries

Before formatting:

import textwrap

def get_sql_query():
    return textwrap.dedent("""
        SELECT users.name, orders.total
FROM users
            JOIN orders ON users.id = orders.user_id
        WHERE orders.status = 'complete'
    """)

Inconsistent indentation inside the string makes it hard to read and understand the actual SQL query structure.

After formatting:

import textwrap

def get_sql_query():
    return textwrap.dedent("""
                SELECT users.name, orders.total
        FROM users
                    JOIN orders ON users.id = orders.user_id
                WHERE orders.status = 'complete'
    """)

โœ… What changed:

  • Inconsistent indentation is normalized
  • Each line's indentation now reflects the SQL structure
  • Trailing whitespace removed
  • The formatted version shows what dedent() will produce at runtime

The key insight: The indentation you see in the source code now matches what dedent() returns. When this code runs, dedent() strips the common leading whitespace, and you get properly formatted SQL.

Example 2: Using --add-dedent mode

Before:

def get_message():
    message = """
Hello World!
This is a message.
"""
    return message

After running with --add-dedent:

from textwrap import dedent
def get_message():
    message = dedent("""
Hello World!
This is a message.
""")
    return message

โœ… What changed:

  1. Detected that the string has no leading whitespace (left-aligned)
  2. Wrapped it with dedent() for consistency
  3. Added the import statement automatically
  4. Reformatted with proper indentation matching the code structure

Why use dedent here? Even though this string doesn't need dedenting now, using dedent() consistently makes it easier to modify the string later. You can add indentation for readability without affecting the runtime output.

Example 3: HTML template formatting

Before:

from textwrap import dedent

def render_html():
    return dedent("""
    <div class="container">
        <h1>Welcome</h1>
            <p>This is a paragraph.</p>
    </div>
    """)

After:

from textwrap import dedent

def render_html():
    return dedent("""
        <div class="container">
            <h1>Welcome</h1>
                <p>This is a paragraph.</p>
        </div>
    """)

Example 4: JSON template

Before:

import textwrap

CONFIG = textwrap.dedent('''
{
  "name": "my-app",
      "version": "1.0.0"
}
''')

After:

import textwrap

CONFIG = textwrap.dedent('''
    {
      "name": "my-app",
          "version": "1.0.0"
    }
''')

Example 5: Error message with inconsistent indentation

Before:

from textwrap import dedent

def validate_user(user):
    if not user.email:
        raise ValueError(dedent("""
            Invalid user configuration:
                - Email is required
            - Must be a valid email address
                - Example: user@example.com
        """))

After:

from textwrap import dedent

def validate_user(user):
    if not user.email:
        raise ValueError(dedent("""
            Invalid user configuration:
                - Email is required
            - Must be a valid email address
                - Example: user@example.com
        """))

โœ… What changed:

  • Mixed indentation levels are now consistent
  • Each line's indentation shows the message structure
  • The formatted version visually matches what users will see at runtime

๐Ÿง  How It Works

  1. Parse โ€” Uses Python's AST module to analyze source code
  2. Find โ€” Locates all dedent() or textwrap.dedent() calls with string arguments
  3. Analyze โ€” Determines the appropriate indentation level based on context
  4. Format โ€” Removes trailing whitespace and applies consistent indentation
  5. Validate โ€” Ensures dedent(original) == dedent(formatted) (behavior unchanged)
  6. Replace โ€” Updates the source file with formatted strings

The key insight: Strings are formatted in the source to visually match their runtime output after dedent() processes them. This makes the code more readable without changing behavior.


๐Ÿ›ก๏ธ Safety & Compatibility

  • Non-destructive โ€” Always validates that dedent(original) == dedent(formatted)
  • Preserves behavior โ€” Formatted strings have identical runtime output
  • Quote style aware โ€” Maintains your choice of """ vs '''
  • Escape handling โ€” Correctly handles backslashes and escape sequences
  • Python 3.8+ โ€” Works with modern Python versions

What gets formatted:

  • โœ… Literal strings inside textwrap.dedent() calls
  • โœ… Literal strings inside dedent() calls (when imported)

What doesn't get formatted:

  • โŒ Regular strings (not in dedent calls)
  • โŒ F-strings (can't be wrapped with dedent)
  • โŒ String concatenations
  • โŒ Docstrings at module level
  • โŒ All other code (completely untouched)

๐Ÿงช Development

Setup

# Clone the repository
git clone https://github.com/15r10nk/format-dedent.git
cd format-dedent

# Install with development dependencies
pip install -e ".[dev]"

# Install pre-commit hooks
pre-commit install

Running Tests

# Run all tests
pytest

# Run with verbose output
pytest -v

# Run specific test file
pytest tests/test_formatter.py

Test Suite

The project includes 51 comprehensive tests covering:

  • โœ… Module, function, and class level dedent formatting
  • โœ… Nested blocks (if, for, try/except)
  • โœ… Quote style preservation (""" and ''')
  • โœ… Backslash line continuations
  • โœ… Real-world examples (SQL, HTML, JSON templates)
  • โœ… Edge cases and error handling
  • โœ… CLI integration tests

Tests use inline-snapshot for snapshot testing.

Project Structure

format-dedent/
โ”œโ”€โ”€ src/format_dedent/
โ”‚   โ”œโ”€โ”€ __init__.py          # Package exports
โ”‚   โ”œโ”€โ”€ __main__.py          # Entry point
โ”‚   โ”œโ”€โ”€ cli.py               # CLI interface
โ”‚   โ”œโ”€โ”€ formatter.py         # String formatting logic
โ”‚   โ”œโ”€โ”€ add_dedent.py        # Add dedent() calls
โ”‚   โ””โ”€โ”€ ast_helpers.py       # AST analysis utilities
โ””โ”€โ”€ tests/
    โ”œโ”€โ”€ test_formatter.py    # Formatting tests
    โ””โ”€โ”€ test_cli.py          # CLI integration tests

๐Ÿ“ License

MIT License - See LICENSE file for details


๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request.


โญ Related Projects

  • Black โ€” The uncompromising Python code formatter
  • inline-snapshot โ€” Snapshot testing for Python

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

format_dedent-0.1.0.tar.gz (21.3 kB view details)

Uploaded Source

Built Distribution

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

format_dedent-0.1.0-py3-none-any.whl (12.8 kB view details)

Uploaded Python 3

File details

Details for the file format_dedent-0.1.0.tar.gz.

File metadata

  • Download URL: format_dedent-0.1.0.tar.gz
  • Upload date:
  • Size: 21.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.7

File hashes

Hashes for format_dedent-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c45b348c6d53943f05a2f3dded7833d8b9309694abacb74790382357121387f2
MD5 8247d4afd15cb0da0ee9572b8b18999e
BLAKE2b-256 913a8c7b024c793f5915e00dbec39398908e4c71c3a95dc91dd8a3c2a4de2a5c

See more details on using hashes here.

File details

Details for the file format_dedent-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for format_dedent-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fb04889eed7918226e0b9f35207dde16a6b786faf3c1b3f3da34fc80618b38aa
MD5 d4406f815327df7e817f54fa79a467cf
BLAKE2b-256 2cdd390468f0e95df35ad0de3342feab325be8c84364b2df7c39164c590c243c

See more details on using hashes here.

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