Apply Black formatting only in regions changed since last commit
—
Helper functions for working with files, directories, and path filtering. These utilities support project discovery, configuration file location, and file filtering based on include/exclude patterns.
Functions for finding project configuration files and roots.
def find_pyproject_toml(path_search_start: tuple[str, ...]) -> str | None:
"""
Find the absolute filepath to a pyproject.toml if it exists.
Searches upward from the given starting paths to locate the project
root and check for pyproject.toml configuration.
Parameters:
- path_search_start: Tuple of starting paths to search from
Returns:
Absolute path to pyproject.toml file, or None if not found
"""Pre-compiled regular expressions for filtering Python files and excluding common directories.
DEFAULT_EXCLUDE_RE: Pattern[str]
"""
Regular expression for default exclusion patterns.
Excludes common directories like:
- .git, .hg, .svn (version control)
- .mypy_cache, .pytest_cache, .ruff_cache (tool caches)
- .tox, .nox (testing environments)
- __pycache__, .eggs (Python artifacts)
- build, dist, _build (build outputs)
- .venv, venv (virtual environments)
- .vscode, .direnv (editor/tool directories)
"""
DEFAULT_INCLUDE_RE: Pattern[str]
"""
Regular expression for default inclusion patterns.
Includes files with extensions:
- .py (Python files)
- .pyi (Python stub files)
- .ipynb (Jupyter notebooks)
"""Functions for processing and filtering file paths.
def filter_python_files_directory(
paths: Collection[Path],
root: Path,
include: Pattern[str],
exclude: Pattern[str],
extend_exclude: Pattern[str] | None,
force_exclude: Pattern[str] | None,
formatter: BaseFormatter,
) -> Iterator[Path]:
"""
Filter Python files in directories based on include/exclude patterns.
Parameters:
- paths: Collection of file/directory paths to process
- root: Root directory for relative path calculations
- include: Pattern for files to include
- exclude: Pattern for files/directories to exclude
- extend_exclude: Additional exclusion pattern
- force_exclude: Pattern that overrides all inclusion rules
- formatter: Formatter instance for additional filtering
Yields:
Path objects for Python files that match the filtering criteria
"""
def gen_python_files(
paths: Collection[Path],
root: Path,
include: Pattern[str],
exclude: Pattern[str],
extend_exclude: Pattern[str] | None = None,
force_exclude: Pattern[str] | None = None,
) -> Iterator[Path]:
"""
Generate Python file paths from given directories and files.
Parameters:
- paths: Paths to files/directories to process
- root: Project root directory
- include: Pattern for files to include
- exclude: Pattern for files/directories to exclude
- extend_exclude: Additional exclusion pattern
- force_exclude: Pattern that overrides inclusion rules
Yields:
Path objects for Python files matching the criteria
"""from darker.files import find_pyproject_toml
# Find pyproject.toml starting from current directory
config_path = find_pyproject_toml((".",))
if config_path:
print(f"Found project config: {config_path}")
# Read configuration
import tomllib
with open(config_path, 'rb') as f:
config = tomllib.load(f)
darker_config = config.get('tool', {}).get('darker', {})
print(f"Darker config: {darker_config}")
else:
print("No pyproject.toml found")from darker.files import DEFAULT_INCLUDE_RE, DEFAULT_EXCLUDE_RE
from pathlib import Path
# Test if files match default patterns
test_files = [
"src/module.py",
"tests/test_module.py",
"setup.py",
"README.md",
".git/config",
"__pycache__/module.pyc",
"dist/package.whl"
]
for file_path in test_files:
path = Path(file_path)
# Check inclusion
includes = DEFAULT_INCLUDE_RE.search(str(path))
# Check exclusion
excludes = DEFAULT_EXCLUDE_RE.search(str(path))
if includes and not excludes:
print(f"✓ Would process: {file_path}")
else:
reason = "excluded" if excludes else "not Python file"
print(f"✗ Would skip: {file_path} ({reason})")import re
from darker.files import gen_python_files
from pathlib import Path
# Create custom patterns
custom_include = re.compile(r'(\.py|\.pyx)$') # Include Cython files
custom_exclude = re.compile(r'/(build|dist|\.tox)/')
custom_extend_exclude = re.compile(r'/legacy/') # Skip legacy code
# Find Python files with custom filters
root = Path(".")
paths = [Path("src"), Path("tests")]
python_files = list(gen_python_files(
paths=paths,
root=root,
include=custom_include,
exclude=custom_exclude,
extend_exclude=custom_extend_exclude
))
print(f"Found {len(python_files)} Python files:")
for file_path in python_files:
print(f" {file_path}")from darker.files import filter_python_files_directory
from darker.formatters import create_formatter
from pathlib import Path
import re
# Set up formatter and patterns
formatter = create_formatter("black")
root = Path(".")
paths = [Path("src"), Path("tests")]
include_pattern = re.compile(r'\.pyi?$')
exclude_pattern = re.compile(r'/(\.git|__pycache__|\.pytest_cache)/')
# Filter files that the formatter can process
filtered_files = list(filter_python_files_directory(
paths=paths,
root=root,
include=include_pattern,
exclude=exclude_pattern,
extend_exclude=None,
force_exclude=None,
formatter=formatter
))
print(f"Files to format with {formatter.name}:")
for file_path in filtered_files:
print(f" {file_path}")from darker.files import gen_python_files, DEFAULT_INCLUDE_RE, DEFAULT_EXCLUDE_RE
from pathlib import Path
def find_all_python_files(start_path: Path) -> list[Path]:
"""Find all Python files recursively from a starting path."""
if start_path.is_file():
# Single file
if DEFAULT_INCLUDE_RE.search(str(start_path)):
return [start_path]
else:
return []
# Directory - search recursively
return list(gen_python_files(
paths=[start_path],
root=start_path,
include=DEFAULT_INCLUDE_RE,
exclude=DEFAULT_EXCLUDE_RE
))
# Usage examples
project_files = find_all_python_files(Path("src"))
test_files = find_all_python_files(Path("tests"))
single_file = find_all_python_files(Path("setup.py"))
print(f"Project files: {len(project_files)}")
print(f"Test files: {len(test_files)}")
print(f"Single file: {len(single_file)}")Install with Tessl CLI
npx tessl i tessl/pypi-darker@3.0.1