This document describes the coding style and patterns used in this project. Follow these conventions when contributing so that the codebase stays consistent and accessible.
Automated enforcement is provided by Ruff (linting). The guidance here covers intent, patterns, and project-specific decisions that go beyond what a linter can check.
-
Target Python 3.10+ in all new code.
-
Use the modern union syntax for optional and union types:
# Correct def foo(name: str | None = None) -> int | str: ... # Wrong from typing import Optional, Union def foo(name: Optional[str] = None) -> Union[int, str]: ...
-
Use built-in generic types directly (
list[str],dict[str, int],tuple[int, ...]) instead ofList,Dict,Tuplefromtyping. -
Annotate all public function parameters and return types.
| Setting | Value |
|---|---|
| Line length | 100 |
| Linter | Ruff |
Run the linter:
uv run ruff check src/folge_cli/
uv run ruff check --fix src/folge_cli/ # auto-fix safe issuesUse double quotes for strings. Ruff normalizes quote style.
Use trailing commas in multi-line collections and function signatures:
result = some_function(
first_argument,
second_argument,
third_argument,
)Active rule sets: E, F, W.
E501 (line too long) is ignored because the project uses a 100-character
line length with Ruff.
| Code | Rule | Notes |
|---|---|---|
F841 |
Unused variable | Remove or replace with _ |
E731 |
Lambda assignment | Use def instead |
B006 |
Mutable default argument | Use None sentinel and assign inside the function |
Follow PEP 8:
| Construct | Convention | Example |
|---|---|---|
| Module | snake_case |
batch_process.py |
| Class | PascalCase |
StepCounter |
| Function / method | snake_case |
resolve_provider |
| Variable | snake_case |
file_path |
| Constant | UPPER_SNAKE_CASE |
PROVIDERS |
| Private helper | leading underscore | _build_auth_headers |
src/folge_cli/
├── __init__.py # Package metadata
├── __main__.py # python -m folge_cli entry point
├── _version.py # Dynamic version (CalVer)
├── cli.py # folge-cli CLI entry point
├── config.py # Centralized configuration loading
├── pipeline.py # Full pipeline orchestrator
├── batch_process.py # Vision API image processing
├── merge.py # Deterministic merge
├── render.py # Markdown rendering
├── publish.py # PDF/UA publishing
├── validate_schema.py # Schema validation
├── validate_content.py # Content quality validation
├── validate_pdf.py # PDF/UA compliance
├── generate_manual_attention.py # Manual attention markdown
└── progress.py # Step counters and progress display
Rules:
config.pyis the single source of truth for configuration — other modules import from it, never read.envorconfig.yamldirectly.cli.pyis the only user-facing entry point; it dispatches to other modules.- Modules should be importable independently (no circular imports).
- Do not add new top-level modules without discussion.
All settings follow a strict resolution order:
# CLI argument > environment variable > config.yaml > hardcoded defaultWhen adding a new setting:
- Add it to
_PROVIDER_DEFSinconfig.py(for provider settings) or create a newget_*()function. - Add the env var to
.envandenvTemplate. - Add the YAML key to
config.yaml. - Document the setting in
README.md.
Ruff enforces this order automatically:
- Standard library (
import os,from pathlib import Path) - Third-party (
import yaml,from dotenv import load_dotenv) - Local (
from folge_cli.config import get_env)
Separate each group with a blank line. Do not mix groups.
- Write NumPy-style docstrings for all public functions and classes.
- Inline comments should explain why, not what. Avoid restating the code.
- Keep TODO comments short and actionable; link to an issue when possible.
# Good
# pdfinfo uses -v (not --version) on most systems; shutil.which handles
# PATH resolution so we don't hardcode /usr/bin/pdfinfo.
# Bad
# check if pdfinfo existsdef function_name(param1, param2=None):
"""Short summary of what the function does.
Longer description if needed. Can span multiple paragraphs.
Parameters
----------
param1 : str
Description of param1.
param2 : int, optional
Description of param2. Default is ``None``.
Returns
-------
bool
Description of return value.
Raises
------
ValueError
When something is wrong.
"""- Tests live in
tests/and are run with pytest. - Test function names describe the scenario:
test_resolve_provider_defaults_to_ollama. - Use
pytest.mark.parametrizefor multiple input cases instead of loops. - Do not mock the file system unless absolutely necessary; use
tmp_pathfixtures.
Run: uv run pytest -q