Configuration schema¶
By default, ConfigOption only feeds configuration values that match CLI options into the context’s default_map. All other keys are silently ignored. This works when the configuration file mirrors the CLI, but some applications need additional configuration that matches no CLI option.
The config_schema parameter extracts the app’s configuration section, normalizes its keys, and produces a typed object available to all commands via ctx.meta["click_extra.tool_config"].
Tip
repomatic is a production CLI that uses all of the features below: a 48-field Config dataclass with nested sub-dataclasses, opaque dict fields for GitHub Actions matrices, config_path metadata for kebab-case TOML keys, and a schema-only section (included_params=()) so unknown keys warn. It can serve as a reference for building complex typed configuration.
Dataclass schema¶
The most common pattern is a Python dataclass. Click Extra auto-detects dataclass types, normalizes hyphenated keys to underscores, flattens nested sections, and filters to known fields:
from dataclasses import dataclass, field
from click_extra import command, echo, group, option, pass_context
from click_extra.config import get_tool_config
@dataclass
class AppConfig:
"""Typed configuration for my-app."""
extra_categories: list[str] = field(default_factory=list)
output_format: str = "text"
@group(config_schema=AppConfig)
@option("--verbose/--no-verbose")
@pass_context
def my_app(ctx, verbose):
"""An app with typed configuration."""
config = get_tool_config(ctx)
if config is not None:
echo(f"output_format: {config.output_format}")
echo(f"extra_categories: {config.extra_categories}")
@my_app.command()
@option("--name", default="World")
def greet(name):
"""Say hello."""
echo(f"Hello, {name}!")
With a TOML configuration file:
~/.config/my-app/config.toml¶[my-app]
verbose = true
extra-categories = ["docs", "tests"]
output-format = "json"
[my-app.greet]
name = "Alice"
The CLI options (verbose, name) are fed into default_map as before. The additional keys (extra-categories, output-format) are normalized (hyphens to underscores) and passed to the AppConfig dataclass. Fields not present in the file get their dataclass defaults.
$ my-app --help
Usage: my-app [OPTIONS] COMMAND [ARGS]...
An app with typed configuration.
Options:
--verbose / --no-verbose [default: no-verbose]
-h, --help Show this message and exit.
Configuration options:
--config LOCATION Location of the configuration file. Supports
local path with glob patterns or remote URL.
[default: ~/.config/my-app/]
--no-config Ignore all configuration files and only use
command line parameters and environment
variables.
--validate-config LOCATION Validate the configuration file and exit.
--export-config FORMAT Export the configuration in the selected format
to <stdout>, then exit.
Output options:
--accessible Accessibility mode: disable colors and render
tables in a borderless, screen-reader-friendly
format.
--color [auto|always|never] Colorize the output. A bare --color is the same
as --color=always. [default: auto]
--no-color Disable colorization (alias of --color=never).
--progress / --no-progress Show progress indicators during long operations.
Disabled for non-interactive output (pipes, dumb
terminals, CI) and by --accessible. [default:
progress]
--theme [auto|dark|dracula|light|manpage|monokai|nord|solarized-dark]
Color theme used for help screens. [default:
dark]
--table-format FORMAT Rendering style of tables. [default: rounded-
outline]
Logging options:
--verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.
[default: WARNING]
-v, --verbose Increase the default WARNING verbosity by one
level for each additional repetition of the
option. [default: 0]
-q, --quiet Decrease the default WARNING verbosity by one
level for each additional repetition of the
option. [default: 0]
--debug Shorthand for --verbosity DEBUG.
Introspection options:
--time / --no-time Measure and print elapsed execution time.
[default: no-time]
--params Show all CLI parameters, their provenance,
defaults and value, then exit.
--tree Show the tree of nested subcommands and exit.
--man Read the command's manual page and exit.
--help-format [carapace|json|json-full|man|markdown|markdown-full]
Render the command in the given format and exit.
--version Show the version and exit.
Commands:
greet Say hello.
help Show help for a command.
/home/runner/work/click-extra/click-extra/click_extra/commands.py:920: UserWarning: The parameter --verbose is used more than once. Remove its duplicate as parameters should be unique.
self._resolve_presentation_eagerly(ctx, args)
/home/runner/work/click-extra/click-extra/.venv/lib/python3.14/site-packages/click/core.py:1369: UserWarning: The parameter --verbose is used more than once. Remove its duplicate as parameters should be unique.
parser = self.make_parser(ctx)
/home/runner/work/click-extra/click-extra/.venv/lib/python3.14/site-packages/click/core.py:1988: UserWarning: The parameter --verbose is used more than once. Remove its duplicate as parameters should be unique.
rest = super().parse_args(ctx, args)
/home/runner/work/click-extra/click-extra/.venv/lib/python3.14/site-packages/click/core.py:1948: UserWarning: The parameter --verbose is used more than once. Remove its duplicate as parameters should be unique.
rv = super().collect_usage_pieces(ctx)
/home/runner/work/click-extra/click-extra/click_extra/highlight.py:324: UserWarning: The parameter --verbose is used more than once. Remove its duplicate as parameters should be unique.
formatter.keywords = self.collect_keywords(ctx)
Callable schema¶
Any callable that accepts a dict and returns an object can be used as config_schema. This supports Pydantic models, attrs classes, or custom factories:
from types import SimpleNamespace
from click_extra import echo, group, pass_context
from click_extra.config import get_tool_config, normalize_config_keys
def parse_config(raw):
"""Custom config parser that normalizes keys."""
return SimpleNamespace(**normalize_config_keys(raw))
@group(config_schema=parse_config)
@pass_context
def callable_app(ctx):
"""An app with a callable schema."""
config = get_tool_config(ctx)
if config is not None:
echo(f"value: {config.custom_value}")
@callable_app.command()
def run():
"""Run the app."""
echo("done")
$ callable-app --help
Usage: callable-app [OPTIONS] COMMAND [ARGS]...
An app with a callable schema.
Options:
-h, --help Show this message and exit.
Configuration options:
--config LOCATION Location of the configuration file. Supports
local path with glob patterns or remote URL.
[default: ~/.config/callable-app/]
--no-config Ignore all configuration files and only use
command line parameters and environment
variables.
--validate-config LOCATION Validate the configuration file and exit.
--export-config FORMAT Export the configuration in the selected format
to <stdout>, then exit.
Output options:
--accessible Accessibility mode: disable colors and render
tables in a borderless, screen-reader-friendly
format.
--color [auto|always|never] Colorize the output. A bare --color is the same
as --color=always. [default: auto]
--no-color Disable colorization (alias of --color=never).
--progress / --no-progress Show progress indicators during long operations.
Disabled for non-interactive output (pipes, dumb
terminals, CI) and by --accessible. [default:
progress]
--theme [auto|dark|dracula|light|manpage|monokai|nord|solarized-dark]
Color theme used for help screens. [default:
dark]
--table-format FORMAT Rendering style of tables. [default: rounded-
outline]
Logging options:
--verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.
[default: WARNING]
-v, --verbose Increase the default WARNING verbosity by one
level for each additional repetition of the
option. [default: 0]
-q, --quiet Decrease the default WARNING verbosity by one
level for each additional repetition of the
option. [default: 0]
--debug Shorthand for --verbosity DEBUG.
Introspection options:
--time / --no-time Measure and print elapsed execution time.
[default: no-time]
--params Show all CLI parameters, their provenance,
defaults and value, then exit.
--tree Show the tree of nested subcommands and exit.
--man Read the command's manual page and exit.
--help-format [carapace|json|json-full|man|markdown|markdown-full]
Render the command in the given format and exit.
--version Show the version and exit.
Commands:
help Show help for a command.
run Run the app.
Retrieving the config object¶
The typed configuration is stored in ctx.meta["click_extra.tool_config"] and can be accessed in two ways:
# Via the convenience helper (uses current context by default):
from click_extra.config import get_tool_config
config = get_tool_config()
# Or directly from the context:
config = ctx.find_root().meta.get("click_extra.tool_config")
If no config_schema was set, get_tool_config() returns None. When a config_schema is configured but no configuration file is found, the schema is instantiated with its defaults so get_tool_config() always returns a usable object.
Format-agnostic¶
The config_schema feature works with every format ConfigOption supports. The parsed configuration is normalized into a Python dict before the schema is applied, so the same schema works regardless of the source format.
For example, the same AppConfig dataclass works with YAML:
~/.config/my-app/config.yaml¶my-app:
extra-categories:
- docs
- tests
output-format: json
Or JSON:
~/.config/my-app/config.json¶{
"my-app": {
"extra-categories": ["docs", "tests"],
"output-format": "json"
}
}
Key normalization¶
Configuration formats commonly use kebab-case (extra-categories), while Python identifiers use snake_case (extra_categories). The normalize_config_keys utility handles this conversion recursively:
from click_extra.config import normalize_config_keys
raw = {"extra-categories": ["a", "b"], "nested-section": {"sub-key": 1}}
normalized = normalize_config_keys(raw)
# {"extra_categories": ["a", "b"], "nested_section": {"sub_key": 1}}
For dataclass schemas, this normalization is applied automatically. For callable schemas, call normalize_config_keys explicitly if needed.
Nested configuration sections¶
TOML and YAML configurations often group related settings under sub-tables (like [tool.myapp.dependency-graph]). When using a dataclass schema, Click Extra automatically flattens these nested sections by joining parent and child keys with _, so they map directly to flat dataclass fields:
from click_extra.config import flatten_config_keys, normalize_config_keys
raw = {"dependency-graph": {"all-groups": True, "output": "deps.mmd"}}
flatten_config_keys(normalize_config_keys(raw))
# {"dependency_graph_all_groups": True, "dependency_graph_output": "deps.mmd"}
This means a dataclass with flat fields like dependency_graph_output and dependency_graph_all_groups can be populated from nested TOML:
[my-app.dependency-graph]
output = "deps.mmd"
all-groups = false
The full pipeline applied to dataclass schemas is: normalize keys (hyphens to underscores), flatten nested dicts (joining with _), then match against dataclass field names. Top-level keys and nested sub-table keys can be mixed freely.
For callable schemas, use flatten_config_keys and normalize_config_keys explicitly if you need the same behavior.
Type-aware flattening¶
By default, flatten_config_keys recurses into every nested dict. This breaks fields typed as dict[str, X] where the dict keys are data rather than config structure (for example, GitHub Actions matrix axis names like os or python-version).
When using a dataclass schema, Click Extra inspects field type hints and automatically stops flattening at dict-typed field boundaries: the same extension-point detection covered in Extending validation, seen from the flattening pipeline’s side. The dict value is assigned whole to the matching field:
from dataclasses import dataclass, field
@dataclass
class AppConfig:
simple_value: str = ""
matrix_axes: dict[str, list[str]] = field(default_factory=dict)
[my-app]
simple-value = "hello"
[my-app.matrix-axes]
python-version = ["3.12", "3.13"]
os = ["ubuntu", "macos"]
Here matrix_axes receives {"python_version": ["3.12", "3.13"], "os": ["ubuntu", "macos"]} as a single dict, rather than being split into matrix_axes_python_version and matrix_axes_os. The pipeline calls this passthrough behavior internally: each extension path is added to an opaque keys set that normalize_config_keys and flatten_config_keys consult before recursing.
Both helpers accept an opaque_keys parameter for manual control, useful when working with raw config dicts outside the schema pipeline:
from click_extra.config import flatten_config_keys
conf = {"matrix": {"replace": {"os": {"old": "new"}}, "count": 3}}
flatten_config_keys(conf, opaque_keys=frozenset({"matrix_replace"}))
# {"matrix_replace": {"os": {"old": "new"}}, "matrix_count": 3}
Field metadata¶
Dataclass fields can carry metadata to control how their values are extracted from the raw config:
click_extra.config_path(alias:CONFIG_PATH_METADATA_KEY): A dotted TOML path (like"test-matrix.replace"). The value is extracted directly from the raw config before normalization and flattening, bypassing the standard pipeline.click_extra.normalize_keys(alias:NORMALIZE_KEYS_METADATA_KEY): Set toFalseto skip key normalization on the extracted value. Useful when the value contains keys that are external identifiers (for example, GitHub Actions axis names likepython-version) that must not be converted topython_version.click_extra.extension(alias:EXTENSION_METADATA_KEY): Set toTrueto declare the field as an extension point. The sub-tree at that field becomes a passthrough: strict-check skips it, the flatten pipeline treats it as opaque, and a registeredConfigValidator(or your own code) takes over its validation. Equivalent to typing the field asdict[str, X]; use the metadata form when the field’s runtime type isn’t a mapping.
from dataclasses import dataclass, field
@dataclass
class AppConfig:
special: dict[str, str] = field(
default_factory=dict,
metadata={
"click_extra.config_path": "deep.section",
"click_extra.normalize_keys": False,
},
)
[my-app.deep.section]
kebab-key = "preserved"
With normalize_keys=False, special receives {"kebab-key": "preserved"} instead of {"kebab_key": "preserved"}.
Nested dataclass schemas¶
Fields whose type is another dataclass are recursively instantiated with the same normalize/flatten/opaque logic. This allows complex config sections to be modeled as typed sub-schemas:
from dataclasses import dataclass, field
@dataclass
class MatrixConfig:
exclude: list[dict[str, str]] = field(default_factory=list)
replace: dict[str, dict[str, str]] = field(default_factory=dict)
variations: dict[str, list[str]] = field(default_factory=dict)
@dataclass
class AppConfig:
name: str = ""
matrix: MatrixConfig = field(
default_factory=MatrixConfig,
metadata={
"click_extra.config_path": "test-matrix",
"click_extra.normalize_keys": False,
},
)
[my-app]
name = "my-project"
[my-app.test-matrix]
exclude = [{os = "windows-11-arm"}]
[my-app.test-matrix.replace]
os = {"ubuntu-slim" = "ubuntu-24.04"}
[my-app.test-matrix.variations]
python-version = ["3.14"]
The matrix field receives a MatrixConfig instance. Because normalize_keys=False, axis names like python-version and runner identifiers like ubuntu-slim are preserved verbatim in the replace and variations dicts.
Nested dataclass fields without config_path metadata are matched by their normalized field name in the flattened config, just like scalar fields. The nesting is detected from the type hint and the sub-dict is recursively processed.
Schema validation¶
By default, configuration keys that don’t match any dataclass field are ignored: the section may legitimately mix CLI parameter keys with schema fields, so an unrecognized key is not necessarily a typo. Two mechanisms tighten this:
When the section is schema-only (
included_params=(), so no CLI parameter is merged from it), any unknown key can only be a typo: lax mode then logs a warning naming it, while still loading the known fields.The
schema_strictparameter goes further and reports a validation error, catching typos and stale configuration entries:
@group(config_schema=AppConfig, schema_strict=True)
def my_app(): ...
Or directly on the config option:
@config_option(config_schema=AppConfig, schema_strict=True)
When schema_strict=True, an unrecognized key stops the run with a critical-level log and exit code 1. The message lists both the unrecognized keys and all valid options:
Configuration validation error: Unknown configuration option(s): typo_field. Valid options: known_field, output_format
Note
schema_strict is separate from the existing strict parameter. strict controls whether config keys that don’t match CLI parameters are rejected; schema_strict validates against dataclass fields instead. The two can be used independently, and both report through the same ValidationError type (see Error reporting).
Coerce a config dict into a dataclass¶
When you load configuration yourself (or expose a [tool.<name>] section consumers fill in), make_schema_callable(MyDataclass) returns a callable that turns a raw dict into a validated MyDataclass instance. It is the same machinery config_option and get_tool_config use under the hood: hyphenated keys are normalized to field names, dotted click_extra.config_path field metadata is honored, and nested dataclasses are coerced recursively.
from dataclasses import dataclass
from click_extra import make_schema_callable
@dataclass
class Forecast:
city: str = "paris"
high_c: int = 0
load = make_schema_callable(Forecast)
load({"city": "lyon", "high-c": 21}) # Forecast(city="lyon", high_c=21)
Pass strict=True to reject keys that match no field. A non-dataclass callable (a Pydantic .model_validate, say) is returned unchanged, and None passes through.
click_extra.config.schema API¶
classDiagram
Exception <|-- ValidationError
tuple <|-- SchemaFieldInfo
Schema-building and validation engine behind config_option and –validate-config.
- click_extra.config.schema.DEFAULT_SUBCOMMANDS_KEY = '_default_subcommands'
Reserved configuration key for specifying default subcommands.
When a group is invoked without explicit subcommands on the CLI, the subcommands listed under this key execute automatically in order. CLI always wins: if the user names subcommands explicitly, the config is ignored.
Example TOML configuration:
[my-cli] _default_subcommands = ["backup", "sync"] [my-cli.backup] path = "/home"
- click_extra.config.schema.PREPEND_SUBCOMMANDS_KEY = '_prepend_subcommands'
Reserved configuration key for prepending subcommands to every invocation.
Unlike
_default_subcommandswhich only fires when no subcommands are given on the CLI,_prepend_subcommandsalways prepends the listed subcommands. This is useful for always injecting adebugsubcommand on a dev machine, for example.Only works with
chain=Truegroups (non-chained groups resolve exactly one subcommand, so prepending would break the user’s intended command).Example TOML configuration:
[my-cli] _prepend_subcommands = ["debug"]
- click_extra.config.schema.EXTENSION_METADATA_KEY = 'click_extra.extension'
Dataclass field metadata flag marking a field as an extension point.
Schema authors set
metadata={EXTENSION_METADATA_KEY: True}on a field when its sub-tree should pass through click-extra’s CLI-parameter strict check and be validated by app-specific logic instead. Equivalent to typing the field asdict[str, X]: both forms are recognized by_collect_opaque_paths_from_schema(the internal pipeline still calls these paths “opaque” since they’re skipped by the normalize/flatten/strict machinery). The metadata form is useful when the underlying Python type is something other than adict(for example, a nested dataclass that nonetheless represents user-extensible content).
- click_extra.config.schema.CONFIG_PATH_METADATA_KEY = 'click_extra.config_path'
Dataclass field metadata key pinning a field to an explicit config sub-path.
Schema authors set
metadata={CONFIG_PATH_METADATA_KEY: "test-suite"}on a field so the dataclass loader (_from_dataclass) reads its value from that dotted path under the app’s configuration section, rather than from a key named after the field. The named counterpart toEXTENSION_METADATA_KEY.
- click_extra.config.schema.NORMALIZE_KEYS_METADATA_KEY = 'click_extra.normalize_keys'
Dataclass field metadata key toggling key normalization on a field’s value.
Defaults to
True. Schema authors setmetadata={NORMALIZE_KEYS_METADATA_KEY: False}to keep a sub-tree’s keys verbatim, so external identifiers (likepython-versionaxis names) are not rewritten to Python-style names (python_version). Read by_from_dataclassalongsideCONFIG_PATH_METADATA_KEY.
- exception click_extra.config.schema.ValidationError(path, message, code=None)[source]
Bases:
ExceptionRaised when a configuration file fails validation.
A single, structured exception type that uniformly carries the dotted
pathof the offending key, a human-readablemessage, and an optionalcodefor programmatic handling. Used by click-extra’s built-in strict-mode check and by every user-registeredConfigValidator, so downstream apps and--validate-configsee the same error shape regardless of who detected the problem.- Parameters:
path (
str) – Dotted path to the offending key, relative to the configuration file root (like"my-cli.managers.winget.cli_searchpath"). An empty string means the error applies to the document as a whole.message (
str) – Human-readable description of the failure. Should be a single sentence, no trailing punctuation, no path repeated.code (
str|None) – Optional machine-readable error code (like"unknown_field") for callers that want to dispatch on error type without parsing the message string.
- class click_extra.config.schema.ConfigValidator(extension_path, validator, description='')[source]
Bases:
objectRegister an app-defined extension validator for one sub-tree of the configuration file.
Apps register validators via the
config_validators=kwarg onConfigOption(or the matching decorator) to extend click-extra’s built-in CLI-parameter strict check with custom validation logic. Each validator targets a single dottedextension_pathrelative to the app’s configuration section. Click-extra passes the matching sub-tree straight through to the registered validator: the strict check skips it, the schema machinery treats it as opaque, and the user’s logic owns the result. The validator runs both during--validate-configand during normal config loading.- Parameters:
extension_path (
str) – Dotted path of the sub-tree the validator owns, relative to the app’s section in the configuration file. For example, an app namedmy-cliwithextension_path="managers"receives the contents of the[my-cli.managers]table.validator (
Callable[[dict[str,Any]],None]) – Callable taking the sub-tree dict and raisingValidationErroron failure. Must be a pure function: no side effects on the click context, no print statements. The caller decides how to surface the error.description (
str) – Optional human-readable summary of what the validator checks. Surfaces in documentation generators that introspect the decorator (like autodoc), and may be reused in--helptext in a future release.
- extension_path: str
- description: str = ''
- click_extra.config.schema.normalize_config_keys(conf, opaque_keys=frozenset({}), _prefix='')[source]
Normalize configuration keys to valid Python identifiers.
Recursively replaces hyphens with underscores in all dict keys, using the same
str.replace("-", "_")transform that Click applies internally when deriving parameter names from option declarations (--foo-barbecomesfoo_bar). Click does not expose this as a public function, so we replicate the one-liner here.Handles the convention mismatch between configuration formats (TOML, YAML, JSON all commonly use kebab-case) and Python identifiers. Works with all configuration formats supported by
ConfigOption.- Parameters:
opaque_keys (
frozenset[str]) – Fully-qualified key names (using"_"as separator) where recursion stops. The key itself is still normalized, but its dict value is kept as-is. Used in tandem withflatten_config_keys’sopaque_keysto protect data dicts (like GitHub Actions matrix axes) from normalization._prefix (
str) – Internal parameter for tracking the accumulated key path during recursion. Callers should not set this.
Todo
Propose upstream to Click to extract the inline
name.replace("-", "_")into a private_normalize_param_namehelper, so downstream projects like Click Extra can reuse it instead of duplicating the transform.
- click_extra.config.schema.flatten_config_keys(conf, sep='_', opaque_keys=frozenset({}), _prefix='')[source]
Flatten nested dicts into a single level by joining keys with a separator.
Useful for mapping nested configuration structures (like TOML sub-tables) to flat Python dataclass fields. After normalization with
normalize_config_keys, the flattened keys match dataclass field names directly:>>> from click_extra.config import ( ... flatten_config_keys, ... normalize_config_keys, ... ) >>> raw = {"dependency-graph": {"all-groups": True, "output": "deps.mmd"}} >>> flatten_config_keys(normalize_config_keys(raw)) {'dependency_graph_all_groups': True, 'dependency_graph_output': 'deps.mmd'}
- Parameters:
sep (
str) – Separator used to join parent and child keys. Defaults to"_"which produces valid Python identifiers when combined withnormalize_config_keys.opaque_keys (
frozenset[str]) – Fully-qualified key names where flattening stops. When the accumulated key matches an entry in this set, the dict value is kept as-is instead of being recursively flattened. This is useful for fields typed asdict[str, X]where the dict keys are data (like GitHub Actions matrix axis names), not config structure._prefix (
str) – Internal parameter for tracking the accumulated key path during recursion. Callers should not set this.
- Return type:
- click_extra.config.schema.get_tool_config(ctx=None)[source]
Retrieve the typed tool configuration from the context.
Returns the object stored under
click_extra.context.TOOL_CONFIGbyConfigOptionwhen aconfig_schemais set, orNoneif no schema was configured or no configuration was loaded.
- class click_extra.config.schema.SchemaFieldInfo(key: str, type_hint: str, default: Any, summary: str, description: str)[source]
Bases:
NamedTupleDocumentation record for one option of a configuration schema.
Produced by
schema_field_infos(). Consumed by theclick:configSphinx directive, and by CLIs building their own configuration reference (ashow-configtable, say) from the same introspection.Create new instance of SchemaFieldInfo(key, type_hint, default, summary, description)
- key: str
Dotted configuration path of the option.
Field names are kebab-cased (
setup_guide→setup-guide) unless the field pins an explicit path throughclick_extra.config.schema.CONFIG_PATH_METADATA_KEY. Nested dataclass fields contribute one segment per nesting level (test-suite.timeout).
- type_hint: str
The field’s type annotation, as written in the schema source.
- default: Any
The field’s default value, taken from a pristine schema instance.
- summary: str
First paragraph of the field’s attribute docstring, collapsed onto a single line. Empty when the field has no docstring or the class source is unavailable (see
field_docstrings()).
- description: str
Full attribute docstring of the field, paragraph breaks preserved.
- click_extra.config.schema.field_docstrings(cls)[source]
Extract attribute docstrings from a class body, keyed by field name.
Attribute docstrings are string literals immediately following an annotated assignment in a class body (the PEP 257 convention used by Sphinx’s autodoc). Python discards them at runtime, so they are recovered by parsing the class source with
ast. Each docstring is cleaned up withinspect.cleandoc(), preserving paragraph breaks.Caution
Returns an empty mapping when the class source is unavailable, as for classes defined in an
exec-ed code block (an interactive session, or the body of aclick:sourceSphinx directive). Import the schema from a real module to get its docstrings documented.
- click_extra.config.schema.schema_field_infos(schema)[source]
Walk a configuration schema dataclass into per-option records.
Introspects the dataclass fields, their type annotations, defaults, and attribute docstrings. Nested dataclass fields expand recursively into dotted keys (
test-suite.timeout), honoringclick_extra.config.schema.CONFIG_PATH_METADATA_KEYat every level. Records are sorted by key, segment-wise, so a sub-table’s options stay contiguous even when another table’s name shares their prefix (workflow.syncsorts beforeworkflow-pins.sync).Defaults are read off a pristine
schema()instance, so every field must carry a default: configuration schemas are default-complete by construction, sincemake_schema_callable()instantiates them from partial user data.- Raises:
TypeError – when
schemais not a dataclass type.- Return type:
- click_extra.config.schema.make_schema_callable(schema, *, strict=False, normalize=True, warn_unknown=False)[source]
Wrap a schema type into a callable that accepts a raw config dict.
Dataclass types (detected via
dataclasses.is_dataclass) are auto-wrapped: keys are normalized (hyphens to underscores), nested dicts are flattened, and the result is filtered to known fields before instantiation. Three schema-aware features refine this process:Type-aware flattening. Fields typed as
dict[str, X]are treated as opaque:flatten_config_keysstops at their boundary so the dict value is kept intact.Field metadata. Dataclass fields may carry
click_extra.config_path(a dotted TOML path like"test-matrix.replace") andclick_extra.normalize_keys(Falseto skip key normalization on the extracted value). Fields with an explicit path are extracted from the raw config before normalization and flattening.Nested dataclass support. Fields whose resolved type is itself a dataclass are recursively processed with the same logic.
Any other callable is returned as-is. The caller is responsible for key normalization if needed.
NonereturnsNone.
- Parameters:
strict (
bool) – IfTrue, raiseValueErrorwhen the config contains keys that do not match any dataclass field (after normalization and flattening).warn_unknown (
bool) – IfTrue(andstrictisFalse), log a warning naming those same unknown keys instead of silently dropping them. Meant for configs whose section is schema-only (no CLI parameter is merged from it, i.e.included_params=()), where any unrecognized key can only be a typo. Applies recursively to nested dataclasses.normalize (
bool) – IfFalse, skipnormalize_config_keyson the remaining config dict. Used internally when recursing into nested dataclasses whose parent opted out of normalization viaclick_extra.normalize_keys = False.
- Return type:
- class click_extra.config.schema.ValidationReport(schema_instance, opaque_subtrees, errors, merged_conf=None)[source]
Bases:
objectOutcome of one pass through
run_config_validation().Bundles everything a caller needs after validating a parsed configuration document: the typed schema instance, the extracted opaque sub-trees, the template-filtered config ready for
default_map, and every error detected across all validation stages.Note
The report holds references to the parsed sub-trees, not copies, so building it is cheap regardless of document size.
- schema_instance: Any | None
Typed object produced by the configured schema callable.
Nonewhen no schema is configured, or when the schema stage raised (in which case the failure is recorded inerrors).
- opaque_subtrees: dict[str, dict[str, Any]]
Extracted extension sub-trees, keyed by dotted path relative to the app section. Only paths actually present in the document appear here, so callers can re-route them to per-path validators or stash them on
ctx.meta.
- errors: tuple[ValidationError, ...]
Every
ValidationErrordetected, in stage order (unknown CLI-flag keys first, then schema errors, then validator failures). Empty on success.With
collect_all=Falsethis holds at most one error: the first failure short-circuits the remaining stages.
- merged_conf: dict[str, Any] | None = None
The CLI-flag-bound configuration merged onto
params_template: the payload_install_default_map()layers into the context’sdefault_map.Nonewhenparams_templatewasNone(no strict check) or the strict check raised. Read it only on a successful report: it is the same valuemerge_default_map()would recompute, so reusing it avoids a second normalize/strip/merge pass.
- property ok: bool
Truewhen no error was detected.
- click_extra.config.schema.run_config_validation(user_conf, *, app_name, params_template, config_schema=None, config_validators=(), fallback_sections=(), schema_strict=False, schema_warn_unknown=False, strict=False, blocked_params=(), collect_all=True)[source]
Validate a parsed configuration document in one schema-driven pass.
This is the module-level entry point that unifies click-extra’s three historical validation paths (CLI-parameter strict check, dataclass schema, and app-registered
ConfigValidatorhooks) behind a single function yielding a single error type. It is deliberately not namedvalidate_config: that name belongs tovalidate_config(), the callback powering the--validate-configflag.Stages, in order:
Normalize. Strip reserved keys and expand dotted keys.
Partition. Split opaque sub-trees (schema extension fields plus every registered validator’s
extension_path) from the CLI-flag-bound content. Extracted sub-trees land inValidationReport.opaque_subtrees.Strict-check the CLI-flag-bound part against
params_template, keeping the merged result asValidationReport.merged_conf(skipped whenparams_templateisNone).Schema-build the app section through the configured callable, producing
ValidationReport.schema_instance.Validate every opaque sub-tree through its registered validator.
- Parameters:
user_conf (
dict[str,Any]) – The full parsed configuration document.app_name (
str) – Name of the app’s section (used to resolve the section and to root opaque paths and error paths at the document level).params_template (
dict[str,Any] |None) – The CLI-parameter template the strict check runs against. PassNoneto skip the strict check entirely (for example, for a schema-only validation).config_schema (
type|Callable[[dict[str,Any]],Any] |None) – Dataclass type or callable describing the typed configuration, orNone.config_validators (
Sequence[ConfigValidator]) – Extension validators to run against opaque sub-trees.fallback_sections (
Sequence[str]) – Legacy section names to try whenapp_nameis absent or empty.schema_strict (
bool) – Reject keys the dataclass schema does not recognize.schema_warn_unknown (
bool) – In lax mode, log a warning naming keys the dataclass schema does not recognize (seewarn_unknowninmake_schema_callable()). Ignored whenschema_strictrejects them outright.strict (
bool) – Reject keys the CLI-parameter template does not recognize.blocked_params (
Iterable[str]) – Fully-qualified IDs of parameters excluded from configuration files, used to sharpen strict-mode error messages (a blocked parameter is reported as such, not as unknown).collect_all (
bool) – WhenTrue(default), run every stage and collect all errors. WhenFalse, the first error short-circuits the rest.
- Return type:
- Returns:
A
ValidationReport.ValidationErroris the single error type recorded by every stage;ValueError/TypeErrorraised by the strict check or schema callable are wrapped into it.