CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-darker

Apply Black formatting only in regions changed since last commit

Pending
Overview
Eval results
Files

formatters.mddocs/

Formatter System

Pluggable formatter system supporting multiple code formatters like Black, Ruff, and Pyupgrade through a common interface and entry point system.

Capabilities

Formatter Plugin Management

Functions for discovering and creating formatter instances.

def get_formatter_entry_points(name: str | None = None) -> tuple[EntryPoint, ...]:
    """
    Get the entry points of all built-in code re-formatter plugins.
    
    Parameters:
    - name: Optional formatter name to filter by
    
    Returns:
    Tuple of entry points for formatter plugins
    """

def get_formatter_names() -> list[str]:
    """
    Get the names of all built-in code re-formatter plugins.
    
    Returns:
    List of available formatter names (e.g., ['black', 'ruff', 'pyupgrade', 'none'])
    """

def create_formatter(name: str) -> BaseFormatter:
    """
    Create a code re-formatter plugin instance by name.
    
    Parameters:
    - name: Name of the formatter to create
    
    Returns:
    Formatter instance implementing BaseFormatter interface
    """

ENTRY_POINT_GROUP: str = "darker.formatter"
"""Entry point group name for formatter plugins."""

Base Formatter Interface

Abstract base class and common interfaces for all formatters.

class BaseFormatter(ABC):
    """
    Abstract base class for code re-formatters.
    
    All formatter plugins must inherit from this class and implement
    the required methods and attributes.
    """
    name: str                    # Human-readable formatter name
    preserves_ast: bool          # Whether formatter preserves AST
    config_section: str          # Configuration section name
    
    def read_config(self, src: tuple[str, ...], args: Namespace) -> None:
        """
        Read configuration for the formatter.
        
        Parameters:
        - src: Source file/directory paths for configuration context
        - args: Command line arguments namespace
        
        Returns:
        None (configuration is stored in self.config)
        """
    
    @abstractmethod
    def run(self, content: TextDocument, path_from_cwd: Path) -> TextDocument:
        """
        Run the formatter on the given content.
        
        Parameters:
        - content: Source code content to format
        - path_from_cwd: Path to the file being formatted (relative to cwd)
        
        Returns:
        Formatted source code content
        """
    
    def get_config_path(self, config: Any) -> Path | None: 
        """Get configuration file path if available."""
        
    def get_line_length(self, config: Any) -> int:
        """Get line length setting from configuration."""
        
    def get_exclude(self, config: Any) -> Pattern[str]:
        """Get file exclusion patterns."""
        
    def get_extend_exclude(self, config: Any) -> Pattern[str]:
        """Get extended file exclusion patterns."""
        
    def get_force_exclude(self, config: Any) -> Pattern[str]:
        """Get forced file exclusion patterns."""

class HasConfig(Generic[ConfigT]):
    """
    Generic base class for formatters with typed configuration.
    
    Type parameter ConfigT represents the formatter's configuration type.
    """
    config: ConfigT

Built-in Formatters

Concrete formatter implementations included with darker.

class BlackFormatter(BaseFormatter):
    """
    Black code formatter plugin interface.
    
    Integrates with Black for Python code formatting with full AST preservation.
    """
    name = "black"
    preserves_ast = True
    config_section = "tool.black"

class RuffFormatter(BaseFormatter):
    """
    Ruff code formatter plugin interface.
    
    Integrates with Ruff's formatting capabilities for fast Python code formatting.
    """
    name = "ruff format"
    preserves_ast = True
    config_section = "tool.ruff"

class PyupgradeFormatter(BaseFormatter):
    """
    Pyupgrade code formatter plugin interface.
    
    Upgrades Python syntax for newer language versions. Does not preserve AST
    due to syntax transformations.
    """
    name = "pyupgrade"
    preserves_ast = False
    config_section = "tool.pyupgrade"  # Custom section

class NoneFormatter(BaseFormatter):
    """
    Dummy formatter that returns code unmodified.
    
    Useful for testing or when only pre-processors (isort, flynt) are desired.
    """
    name = "dummy reformat"
    preserves_ast = True
    config_section = "tool.none"

Configuration Types

Type definitions for formatter configuration.

class FormatterConfig(TypedDict, total=False):
    """Base class for formatter configuration."""
    line_length: int
    target_version: str

class BlackCompatibleConfig(FormatterConfig, total=False):
    """Configuration for Black-compatible formatters."""
    skip_string_normalization: bool
    skip_magic_trailing_comma: bool
    preview: bool

class BlackModeAttributes(TypedDict, total=False):
    """Type definition for Black Mode configuration items."""
    target_versions: Set[TargetVersion]
    line_length: int
    string_normalization: bool
    magic_trailing_comma: bool
    preview: bool

Configuration Utilities

Helper functions for reading and validating formatter configuration.

def validate_target_versions(target_versions: List[str]) -> Set[TargetVersion]:
    """
    Validate target-version configuration option.
    
    Parameters:
    - target_versions: List of target version strings
    
    Returns:
    Set of validated TargetVersion enum values
    """

def read_black_compatible_cli_args(
    config: BlackCompatibleConfig,
    line_length: int,
) -> List[str]:
    """
    Convert Black-compatible configuration to CLI arguments.
    
    Parameters:
    - config: Black-compatible configuration dictionary
    - line_length: Line length setting
    
    Returns:
    List of CLI arguments for the formatter
    """

Usage Examples

Basic Formatter Usage

from darker.formatters import create_formatter, get_formatter_names
from darkgraylib.utils import TextDocument

# List available formatters
formatters = get_formatter_names()
print(f"Available formatters: {formatters}")

# Create a Black formatter
black_formatter = create_formatter("black")
print(f"Formatter: {black_formatter.name}")
print(f"Preserves AST: {black_formatter.preserves_ast}")

# Read configuration
config = black_formatter.read_config(
    src=("src/",),
    config="pyproject.toml"
)

# Format code
source_code = TextDocument.from_lines([
    "def hello(  name  ):",
    "    print( f'Hello {name}!' )",
    ""
])

formatted_code = black_formatter.run(source_code, config)
print(formatted_code.string)

Custom Formatter Plugin

from darker.formatters.base_formatter import BaseFormatter
from darkgraylib.utils import TextDocument
from typing import Any

class MyFormatter(BaseFormatter):
    """Custom formatter example."""
    
    name = "my-formatter"
    preserves_ast = True
    config_section = "tool.my-formatter"
    
    def read_config(self, src: tuple[str, ...], config: str) -> dict:
        """Read custom configuration."""
        # Load configuration from file or use defaults
        return {
            'line_length': 88,
            'indent_size': 4,
        }
    
    def run(self, content: TextDocument, config: dict) -> TextDocument:
        """Apply custom formatting."""
        # Implement custom formatting logic
        lines = content.lines
        formatted_lines = []
        
        for line in lines:
            # Example: ensure proper indentation
            stripped = line.lstrip()
            if stripped:
                indent_level = (len(line) - len(stripped)) // config['indent_size']
                formatted_line = ' ' * (indent_level * config['indent_size']) + stripped
            else:
                formatted_line = ''
            formatted_lines.append(formatted_line)
        
        return TextDocument.from_lines(formatted_lines)

# Register the formatter (would typically be done via entry points)
# This is just for illustration
formatter = MyFormatter()

Formatter Configuration

from darker.formatters import create_formatter
from darker.formatters.formatter_config import BlackCompatibleConfig

# Create formatter with specific configuration
formatter = create_formatter("black")

# Custom configuration
custom_config: BlackCompatibleConfig = {
    'line_length': 100,
    'skip_string_normalization': True,
    'skip_magic_trailing_comma': False,
    'target_version': 'py39',
    'preview': False,
}

# Read configuration from file
config = formatter.read_config(
    src=("src/", "tests/"),
    config="pyproject.toml"
)

# Check configuration properties
line_length = formatter.get_line_length(config)
exclude_pattern = formatter.get_exclude(config)
print(f"Line length: {line_length}")
print(f"Exclude pattern: {exclude_pattern.pattern}")

Entry Point Registration

To register a custom formatter plugin, add to your package's setup.py or pyproject.toml:

[project.entry-points."darker.formatter"]
my-formatter = "mypackage.formatters:MyFormatter"
# setup.py equivalent
entry_points={
    "darker.formatter": [
        "my-formatter = mypackage.formatters:MyFormatter",
    ],
}

Working with Formatter Results

from darker.formatters import create_formatter
from darkgraylib.utils import TextDocument

formatter = create_formatter("black")
config = formatter.read_config(("src/",), "pyproject.toml")

original = TextDocument.from_lines([
    "def poorly_formatted(x,y):",
    "  return x+y"
])

formatted = formatter.run(original, config)

if original != formatted:
    print("Code was reformatted:")
    print(f"Original:\n{original.string}")
    print(f"Formatted:\n{formatted.string}")
else:
    print("No formatting changes needed")

Install with Tessl CLI

npx tessl i tessl/pypi-darker

docs

chooser.md

command-line.md

configuration.md

diff-utilities.md

file-utilities.md

formatters.md

git-integration.md

index.md

main-functions.md

preprocessors.md

verification.md

tile.json