Creating a Preset¶
A Preset is a Python package that bundles action and handler declarations into a reusable, distributable configuration. Teams use presets to standardize tooling across projects without duplicating config.
1. Create the package¶
pyproject.toml:
[project]
name = "my_preset"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [] # no runtime dependencies needed for a preset-only package
[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.build_meta"
my_preset/__init__.py — can be empty:
What goes inside a preset package¶
A preset package contains:
- A
preset.tomldeclaring action and handler registrations. - For cross-language feature presets (e.g.
fine_format,fine_lint): the definitions of the inter-language action contracts they register. The action class and its canonical registration ship and version together. See ADR-0036. - Orchestration handlers — handler implementations that coordinate or dispatch work to language-specific handlers (e.g.
FormatHandler,LintFilesDispatchHandler). These handlers naturally belong in the preset that defines the action they implement. See Handlers in presets below. - Optionally, an
__init__.pyre-exporting action classes and handler classes for convenient imports.
Preset packages must stay lightweight in their runtime dependencies: only finecode_extension_api (for base classes) and other feature presets (when an action contract references types defined elsewhere) are allowed. Heavy tool dependencies belong to extension packages and are pulled in through dependency groups when handlers are activated.
2. Declare actions in preset.toml¶
The preset.toml file lives inside the package directory (next to __init__.py). It uses the same [tool.finecode.*] syntax as pyproject.toml.
# my_preset/my_preset/preset.toml
[tool.finecode.action.lint]
source = "finecode_extension_api.actions.lint.LintAction"
handlers = [
{ name = "ruff", source = "fine_python_ruff.RuffLintFilesHandler", env = "dev_no_runtime", dependencies = [
"fine_python_ruff~=0.2.0",
] },
{ name = "mypy", source = "fine_python_mypy.MypyLintFilesHandler", env = "dev_no_runtime", dependencies = [
"fine_python_mypy~=0.3.0",
] },
]
[tool.finecode.action.format_python_file]
source = "finecode_extension_api.actions.FormatPythonFileAction"
handlers = [
{ name = "ruff", source = "fine_python_ruff.RuffFormatFileHandler", env = "dev_no_runtime", dependencies = [
"fine_python_ruff~=0.2.0",
] },
{ name = "save", source = "fine_format.SaveFormatFileHandler", env = "dev_no_runtime", dependencies = [
"fine_format~=0.1.0",
] },
]
# Set default handler configs
[[tool.finecode.action_handler]]
source = "fine_python_ruff.RuffLintFilesHandler"
config.extend_select = ["B", "I"]
config.line_length = 88
3. Use the preset in a project¶
Install the preset package (e.g. from PyPI or a local path) into the dev_workspace dependency group:
# User's pyproject.toml
[dependency-groups]
dev_workspace = [
"finecode==0.3.*",
"my_preset==0.1.*",
]
[tool.finecode]
presets = [{ source = "my_preset" }]
Then run:
4. Pin tool versions¶
Universal presets (presets intended for broad adoption across many projects) are responsible for declaring an explicit tool version pin — not just a range. A pin communicates "this version has been validated" and gives all consuming projects a consistent, deterministic tool version automatically.
Declare the pin in preset.toml using the extension-level override:
# my_preset/my_preset/preset.toml
[tool.finecode.extension.fine_python_ruff]
dependencies_override = ["ruff==0.9.0"]
Preset author responsibilities:
- Pin to the latest version you have tested against your preset's handler configurations. Do not pin speculatively.
- When a new tool version requires handler changes, update the extension package version range, update the extension handlers if needed, and bump the pin here in the same preset release.
- When a new tool version is backwards-compatible (no handler changes needed), you may bump the pin in a patch release of the preset.
- Document the tool version history in your changelog so consumers can see which tool versions a given preset version supports.
Preset author — release cycle note: By pinning a tool version, you couple the preset's release cadence to the tool's release cadence. This is intentional for a universal preset: upgrades become deliberate decisions rather than silent resolver choices.
Project-specific presets (e.g. a company-internal preset) may override a universal preset's pin by re-declaring the same extension entry. Users can do the same in their project pyproject.toml. Later configuration layers always win — see Creating an Extension — How overrides are resolved.
5. Handlers in presets¶
A preset can include handler implementations alongside its action definitions. This is the right home for handlers that orchestrate or dispatch work rather than wrapping a specific tool.
Which handlers belong in a preset vs. an extension¶
| Belongs in preset | Belongs in extension |
|---|---|
Orchestrates calls to language-specific subactions (e.g. FormatHandler fans out FormatFilesAction per language) |
Wraps a specific external tool (e.g. RuffLintFilesHandler calls ruff) |
Dispatches to language-specific handlers (e.g. LintFilesDispatchHandler) |
Needs heavy third-party dependencies (ruff, mypy, black, etc.) |
Only depends on finecode_extension_api + other presets |
Needs tool-specific libraries at import time |
Dependency constraint¶
Handlers living in a preset are subject to the same dependency rule as the rest of the preset: only finecode_extension_api and other feature presets are allowed as runtime dependencies. Handler code is imported by the Extension Runner in the handler's declared env, but the preset package is also installed in dev_workspace for config resolution. If a handler module imported a heavy library at the top level, that library would need to be present in dev_workspace too — which is exactly what presets are designed to avoid.
If a handler needs heavy dependencies, it belongs in a separate extension package.
Referencing handlers from the same preset¶
When a handler lives inside the preset, the preset.toml references it by the preset's own import path. The handler still declares a dependencies entry pointing to its own preset package so that prepare-envs installs it in the handler's env:
# fine_format/fine_format/preset.toml
[tool.finecode.action.format]
source = "fine_format.FormatAction"
handlers = [
{ name = "format", source = "fine_format.FormatHandler", env = "dev_no_runtime", dependencies = [
"fine_format~=0.1.0",
] },
]
Package layout with handlers¶
fine_format/
pyproject.toml
fine_format/
__init__.py
preset.toml
format_action.py # action definition
format_handler.py # orchestration handler
6. Allow users to override your defaults¶
Users can add [[tool.finecode.action_handler]] entries in their own pyproject.toml to override any config you set in preset.toml. Your preset's values are the baseline; user config always wins.
Users can also:
- Add more handlers to actions you declared
- Replace all handlers with
handlers_mode = "replace" - Disable specific handlers with
disabled = true
7. Composing multiple presets¶
A project can activate multiple presets. They are applied in order, and later preset handlers are added after earlier ones:
A preset can itself reference other presets in its preset.toml if needed.
Package naming¶
Preset package names follow the pattern fine_<lang?>_<role>, where <role> is drawn from the closed set of FineCode role words and the language segment is optional.
See Package Naming for the shared extension and preset naming convention.
Publishing¶
Presets are regular Python packages — publish them to PyPI with any standard build tool: