FP1: Hierarchical configuration

Rather than using YAML, which is prone to errors and ambiguous grammar, or JSON, which is impossible* to read and modify by a user, this proposes a new config format based on TOML format. This allows logically nested configuration without a physically nested data format, and uses a better specified format with good support in Python (as tomli in PyPI, or tomllib in stdlib with 3.11).

This specific proposal was designed with the need for overriding global or default values on any number of subpaths, so that individual paths (or submodules), within a multirepo or monorepo can have their own custom configuration or lint rules.

Furthermore, individual projects should be able to have their own config files (with relative paths) that are also read and override global config, to provide predictable and consistent linting results when linting the project separately or within the monorepo.

Structure

Configuration at any given path must be located either in the standardized pyproject.toml file, or a separate fixit.toml file. To follow PEP 518, everything in pyproject.toml must be under the tool.<name> table, which would be tool.fixit, so we should use that same table name in fixit.toml for consistency.

Selection of rules could ideally be a simple set of enabled and disabled rules, but it also makes sense to allow specifying groups of rules by their package/module name, as well as their fully qualified names. This is similar to enabling an entire group of lint rules in flake8 with select = E,F vs single rules with ignore = E501.

Subpath overrides

Overriding global/default values should be possible both via additional tables in a top-level config file, or by separate config files within those subpaths. Overrides should be applied to all files within the relative subpaths, accounting for further nested overrides.

It may be worth supporting an inherit = False or root = true type of setting, to ignore all parent/global configs, and prevent inconsistent results when, eg, linting an OSS project exported to Github vs within the originating monorepo.

Overrides should share key names with the global/default values whenever possible. When inheriting parent values, subpath overrides should generally be set unions with parent values. Further semantics/heuristics may need to be applied when a subpath attempts to enable a rule that is otherwise disabled by global or parent configuration.

Proposed examples

Global/default configuration:

[tool.fixit]
inherit = false  # ignore all configs above this one
enable = [
    "fixit",  # enable everything from a top-level package
    "fixit.core",  # only rules from a specific module
    "fixit.core.OneRule",  # enable a specific rule by fully qualified name
]
disable = [
    "fixit.opinions",  # disable an entire module
    "fixit.style.LineLength",  # disable a specific rule by fully qualified name
]

Overrides, option A, array-of-tables:

[[tool.fixit.overrides]]
path = "foo"

# add to the set of enabled/disabled rules
enable = [
    "foo.rules",  # enable a local module with multiple rules
]
disable = [
    "fixit.core.RuleFour",  # disable a core rule by fully qualified name
]

[[tool.fixit.overrides]]
path = "foo/bar/baz"

enable = ["..."]
disable = ["..."]

Overrides, option B, paths-within-table-names:

[tool.fixit.overrides.foo]
enable = ["..."]
disable = ["..."]

[tool.fixit.overrides."foo/bar/baz"]
enable = ["..."]
disable = ["..."]