Configuration model¶
Configuration is the product. This document sketches its shape; exact option names are provisional and will be settled by tests-first API work.
File formats¶
TOML is the primary format; YAML and package.json are supported with the
same schema and keys. See
ADR 0001 for the rationale.
| File | Format |
|---|---|
rainbow.toml, .rainbow.toml |
TOML |
rainbow.yaml, rainbow.yml, .rainbow.yaml, .rainbow.yml |
YAML (1.2 core schema) |
pyproject.toml — [tool.rainbow] table |
TOML |
package.json — "rainbow" key |
JSON |
More than one rainbow configuration in the same directory is an error; rainbow-fmt never picks one silently.
Finding the configuration¶
For each formatted file, rainbow-fmt looks in the file's directory, then in
each parent directory up to the filesystem root, and uses the nearest
configuration. A pyproject.toml without [tool.rainbow] or a
package.json without "rainbow" does not count. Configurations in
directories further up are not merged in; combine files explicitly with
extends.
extends¶
# rainbow.toml
extends = ["../team/style.yaml", "local.toml"] # or a single path
Paths are relative to the file that names them and may be in any
supported format (a pyproject.toml or package.json contributes its
rainbow section). Extended files may extend others; a cycle is an error.
Later files override earlier ones, and the extending file overrides them
all:
- tables are merged key by key;
[[override]]and[[rule]]entries are concatenated, base entries first (so the extending file's rules win);- every other value, arrays included, is replaced.
Files¶
[files]
exclude = ["tests/fixtures/**", "*.generated.json"] # skipped when a directory is walked
ignore_unknown = true # no warning for explicitly named files of unknown type
editorconfig = false # do not read .editorconfig files (default: true)
verify = false # skip verifying formatted output (default: true; see cli.md)
cache = false # neither skip nor record formatted files (default: true)
jobs = 1 # worker processes; 0 (default) is one per CPU (working directory's configuration)
See cli.md.
Values, positions and errors¶
Every format loads into the same data: tables, arrays, strings, integers,
floats and booleans. null (YAML, JSON) and dates (TOML, YAML) are
rejected, and so are duplicate keys. Every value keeps its position, so
errors name the file, line and column (in characters):
rainbow.yaml:2:16: core.line_breaks: expected one of "preserve", "off", "fit"; got boolean false; if you meant the string "off", quote it
rainbow.toml:4:1: rule[0] (json/pair): unknown name 'nope'
TOML is read with the standard library's tomllib; YAML with
ruamel.yaml (1.2 core schema); package.json with a small built-in
parser that tracks positions (ADR 0001, amendment 1).
Example¶
# rainbow.toml
preset = "rainbow:balanced" # starting point; optional
[core]
max_width = 100
indent_style = "space" # "space" | "tab" | "preserve"
indent_size = 4
line_ending = "lf" # "lf" | "crlf" | "preserve"
final_newline = true
[shared] # cross-language vocabulary
quotes = "single" # "single" | "double" | "preserve" | "fewest-escapes"
trailing_comma = "multiline" # "never" | "always" | "multiline" | "preserve"
blank_lines = { max = 2, between_top_level = "preserve" }
align = { assignments = true, object_values = false }
[language.python]
trailing_comma = "never" # overrides [shared] for Python only
bracket_wrap = "fit" # "magic_trailing_comma" | "fit" | "preserve"
top_level_blank_lines = 2
[language.javascript]
semicolons = "as-needed" # "always" | "as-needed" | "preserve"
arrow_parens = "preserve"
brace_style = "1tbs" # "1tbs" | "allman" | "stroustrup" | "preserve"
[language.svelte]
indent_script_and_style = false
[[override]] # path-glob overrides, applied in order
files = ["legacy/**/*.js"]
core.indent_size = 2
language.javascript.semicolons = "preserve"
[[rule]] # user-level rule override (ADR 0005)
language = "css"
select = "declaration"
template = "[field('property'), ': ', field('value'), ';']"
The same configuration (abridged) in YAML:
# rainbow.yaml
preset: rainbow:balanced
core:
max_width: 100
indent_style: space
indent_size: 4
shared:
quotes: single
trailing_comma: multiline
blank_lines: { max: 2, between_top_level: preserve }
language:
python:
bracket_wrap: fit
javascript:
semicolons: as-needed
override:
- files: ["legacy/**/*.js"]
core: { indent_size: 2 }
language: { javascript: { semicolons: preserve } }
Note: YAML is read with the 1.2 core schema, so unquoted off, no, and
on are strings, not booleans. A boolean supplied where an enum is expected
is rejected with a message suggesting quotes.
Option schema¶
Each option is declared once, with a name, a type (integer with a minimum,
a choice of strings, or a boolean), a default and a one-sentence
description (rainbow_fmt.options). Core, shared and [files] options are
declared centrally; each language pack declares its own
(extending.md). Planned: node scope for inline
directives, and before/after examples for generated reference docs.
Options available today:
| Key | Values | Default |
|---|---|---|
core.max_width |
integer ≥ 1 | 80 |
core.indent_style |
"space", "tab" |
"space" |
core.indent_size |
integer ≥ 0 | 4 |
core.tab_width |
integer ≥ 1 | 4 |
core.line_ending |
"lf", "crlf", "preserve" (the input's first line ending) |
"lf" |
core.max_blank_lines |
integer ≥ 0: blank lines kept between members, at most (none are added) | 1 |
shared.trailing_comma |
"never", "multiline" |
"never" ("multiline" for Python, JavaScript and TypeScript) |
language.json.trailing_comma |
"never" (JSON has no trailing commas) |
"never" |
language.json.object_wrap |
"preserve", "fit", "always" |
"preserve" |
language.json.align_values |
boolean | false |
language.css.selector_list |
"one_per_line", "fit" |
"one_per_line" |
language.css.rule_wrap |
"always", "fit", "preserve" |
"always" |
language.css.last_semicolon |
"always", "never", "preserve" |
"always" |
language.python.bracket_wrap |
"magic_trailing_comma", "fit", "preserve" |
"magic_trailing_comma" |
language.python.binary_operator_break |
"before", "after" |
"before" |
language.python.definition_blank_lines |
"enforce", "cap", "preserve" |
"enforce" |
language.python.top_level_blank_lines |
integer ≥ 0 | 2 |
language.python.nested_blank_lines |
integer ≥ 0 | 1 |
language.python.inline_comment_spaces |
integer ≥ 1 | 2 |
language.python.statement_blank_lines |
"preserve", "separate" (blank lines around loops and after if statements in functions) |
"preserve" |
language.python.docstrings |
"preserve", "aligned" (closing quotes on their own line, continuation lines under the first letter) |
"preserve" |
language.python.bracket_hug |
boolean: a list of dicts as [{ … }, { … }] |
false |
language.javascript.return_semicolons |
"as_statements", "unless_last" (a return followed by another statement ends with ;) |
"as_statements" |
language.javascript.bracket_hug |
boolean: an array of objects as [{ … }, { … }] |
false |
language.html.whitespace_sensitivity |
"css", "strict", "ignore" (html.md) |
"css" |
language.html.bracket_same_line |
boolean | false |
language.html.void_elements |
"no_slash" (<br>), "slash" (<br />), "preserve" |
"no_slash" |
language.svelte.whitespace_sensitivity, language.svelte.bracket_same_line |
as for HTML (svelte.md) |
|
language.yaml.sequence_indent |
"indent", "none" (yaml.md) |
"indent" |
language.sql.keyword_case |
"preserve", "upper", "lower" (sql.md) |
"preserve" |
language.markdown.fenced_code |
"format", "preserve" (markdown.md) |
"format" |
language.markdown.table_width |
integer ≥ 1 (markdown.md) |
180 |
files.exclude |
list of patterns (as in [[override]] files, relative to the configuration file): files and directories a directory walk skips; a preset's patterns add to the project's; a file named on the command line is always formatted |
[] |
files.ignore_unknown |
boolean | false |
files.editorconfig |
boolean | true |
files.verify |
boolean | true |
files.cache |
boolean | true |
files.jobs |
integer ≥ 0 (0: one per CPU) | 0 |
Any core or shared option may also be set in a [language.X] section, for
that language only. Language options may appear only in their language's
section.
A language may narrow a core or shared option, allowing fewer values.
The option then belongs to the language: values in [core] or [shared]
(and --set core.…/shared.…) do not apply to it, and a disallowed value
in [language.X] is an error. JSON narrows trailing_comma to "never",
because trailing commas are not valid JSON; [shared] trailing_comma =
"multiline" applies to other languages only.
A language may also change only the default of a core or shared
option. Values in [core] and [shared] still apply to it. Python's
default for trailing_comma is "multiline" (PEP 8, Black); [shared]
trailing_comma = "never" applies to Python too. rainbow-fmt options
shows such a default as language.python.trailing_comma.
Everything is validated when a file is formatted: unknown keys, wrong
types and malformed [[override]] sections are errors that name the file,
line and column. Sections for languages rainbow-fmt does not know (for
example [language.html] today) are ignored, so a configuration can be
written before a language pack exists.
Resolution order (lowest → highest precedence)¶
- Built-in defaults from the schema
- Presets — named bundles:
rainbow:balanced,rainbow:minimal,rainbow:black,rainbow:prettierandrainbow:styleguide(team presets published as packages are planned) .editorconfig(mapped to core options)- Project config: any file listed under File formats —
nearest directory wins;
extendscombines files (a target may be in a different format) [[override]]sections whosefilespatterns match, in order (later sections win)- Inline directives in the source (
rainbow: set, for one member) - CLI flags (
--set language.python.bracket_wrap=fit)
A higher level always wins, whatever section it is written in. Within a
level, [language.X] beats [shared] beats [core] for the same
concept. For example, an override's core.indent_size beats the project's
[language.json] indent_size.
Presets¶
preset = "rainbow:balanced" # or a list; later presets win
| Preset | Sets |
|---|---|
rainbow:balanced |
core.max_width = 100, core.indent_size = 2, core.line_ending = "lf", shared.trailing_comma = "never", language.json.object_wrap = "fit" |
rainbow:minimal |
core.max_width = 120, core.line_ending = "preserve", language.json.object_wrap = "preserve" |
rainbow:black |
Black's layout where an option reaches it: 88 columns, 4 spaces, trailing_comma = "multiline", the magic trailing comma, blank lines enforced; strings, parentheses and docstrings stay as written, unlike Black |
rainbow:prettier |
Prettier's defaults where an option reaches them: 80 columns, 2 spaces, semicolons, bracket_spacing, object_wrap = "preserve", void_elements = "slash"; quotes and parentheses stay as written, and a line Prettier would break inside a tag stays long |
rainbow:styleguide |
This repository's STYLEGUIDE.md (how it was derived): 4 spaces, 99 columns (120 for HTML and Svelte), trailing_comma = "multiline", blank lines between the thoughts of a function, aligned docstrings, bracket_hug, semicolons only after a return that is not last, void_elements = "no_slash"; Django migrations directories excluded |
Presets are configuration files shipped with rainbow-fmt
(rainbow_fmt/presets/); their contents grow with the option set. No
preset applies unless the configuration names one (ADR 0006). preset is an ordinary
top-level setting, so it passes through extends; a preset cannot itself
name presets, and only the built-in rainbow: names are accepted.
.editorconfig¶
For each file, .editorconfig files are read from the file's directory
upwards, stopping after one with root = true; sections whose glob
matches the file apply, later sections and nearer files winning
(EditorConfig semantics, including **,
{a,b} and {1..3} in globs).
| EditorConfig property | Option |
|---|---|
indent_style |
core.indent_style |
indent_size |
core.indent_size (tab is ignored) |
tab_width |
core.tab_width |
max_line_length |
core.max_width (off is ignored) |
end_of_line |
core.line_ending (cr is ignored) |
Invalid values are ignored, as the EditorConfig specification requires, and
unset removes an earlier value. .editorconfig is below the project
configuration: a value in rainbow.toml wins. Turn it off with
[files] editorconfig = false.
[[override]]¶
[[override]]
files = ["legacy/**/*.json", "*.jsonc"]
core.indent_size = 2
language.json.object_wrap = "fit"
files is required. Patterns are matched against the file's path relative
to the directory of the configuration file that contains the override: *
matches within one directory, ? one character, [abc] one of a set
([!abc] none of it), and ** any number of directories. A pattern
without / matches the file name in any directory. An override may contain
core, shared and language sections.
--set¶
rainbow-fmt format --set core.max_width=100 --set shared.trailing_comma=multiline .
sets options for every file, above all configuration files. The value is a
TOML value; a bare word is a string. [files] settings cannot be set this
way (use -u for ignore_unknown, --no-verify for verify, --no-cache for
cache, --jobs for jobs).
Seeing the result¶
rainbow-fmt options FILE prints the options that apply to FILE and where
each value came from (cli.md).
Inline directives¶
Comments in the source can switch formatting off, skip one member, or
change options for one member. They use each language's comment syntax
(#, //, /* … */):
# rainbow: off
MATRIX = [
1, 0, 0,
0, 1, 0,
0, 0, 1,
]
# rainbow: on
x = compute( ) # rainbow: skip-line
# rainbow: set trailing_comma=never
def f(a, b, c): ...
| Directive | Where | Effect |
|---|---|---|
rainbow: off … rainbow: on |
own lines | everything between is printed as written; without on, to the end of the block, object or list |
rainbow: skip-next |
own line | the next member is printed as written |
rainbow: skip-line |
after code | the member it follows is printed as written (for a Python if, def …: its header) |
rainbow: set KEY=VALUE … |
own line | the next member is formatted with these option values |
A member is a Python, JavaScript or TypeScript statement, a class
member, an interface or type literal member, or an item of a bracketed
list, a JSON
object or array member (or the document's value), a CSS rule, at-rule or
declaration. KEY is an option name without its section
(trailing_comma, bracket_wrap); VALUE is written as for --set.
Options the printer applies to the whole file (max_width,
indent_style, indent_size, tab_width, line_ending) cannot be set
by a directive. Python and JavaScript statements printed as written move
as a whole to their block's indentation; their relative indentation,
multi-line strings, template literals and JSX are kept.
Other tools' comments are honoured too: Black's # fmt: off,
# fmt: on and # fmt: skip (like skip-line) in Python, and Prettier's
prettier-ignore (like skip-next) in JSON, CSS, JavaScript and
TypeScript. Where
they cannot apply, they are ordinary comments.
A rainbow: comment that is not a valid directive (# rainbow: of; a
comment of several words whose first is no directive, such as
# rainbow: a formatter, is prose and left alone), is on the wrong kind of
line, has nothing to act on (skip-next at the end of a
block, on without off) or sits where no member is (inside an
expression) is an error: the file is left unchanged and the error names
its line and column:
error: app.py:12:5: unknown directive 'rainbow: of', not formatted
preserve¶
preserve means: for this decision, reproduce what the source did. It is
implemented per option by consulting the trivia and tokens recorded in the
CST. Examples:
quotes = "preserve"keeps each string's original quote character.trailing_comma = "preserve"keeps a trailing comma if and only if one was present.line_breaks = "preserve"treats every source newline inside a construct as a hard break, and never joins lines.blank_lines.* = "preserve"keeps the original count (still capped byblank_lines.max, if set).
Provenance and explain¶
Every resolved value records its origin: the default, a preset, an
.editorconfig line and section, a configuration position (and the
override[N] it belongs to), or --set. rainbow-fmt options FILE
shows them today; explain (planned) will show them per source position:
$ rainbow-fmt explain src/app.py:42
line 42, col 5 string literal
quotes = "double"
set by rainbow.toml:14 [language.python]
overrides [shared] quotes = "single" (rainbow.toml:9)
rule python/string (rainbow_fmt.languages.python.format:88)