Apply Black formatting only in regions changed since last commit
—
Pluggable formatter system supporting multiple code formatters like Black, Ruff, and Pyupgrade through a common interface and entry point system.
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."""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: ConfigTConcrete 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"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: boolHelper 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
"""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)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()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}")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",
],
}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@3.0.1