click_extra.config package

Configuration file loading, format parsing, and schema validation.

This package gathers the three layers behind --config, --no-config, and --validate-config:

  • formats: the supported file formats and their stateless content parsers.

  • schema: the schema-building and validation engine.

  • option: the option classes and click-extra’s own configuration schemas.

Every public symbol is re-exported here so consumers can keep importing from click_extra.config.

class click_extra.config.ClickExtraConfig(test_plan=<factory>, prebake=<factory>)[source]

Bases: object

Schema for the [tool.click-extra] configuration section.

Wired as the config_schema of the top-level click-extra group, so every subcommand reads the same section and pulls its own sub-table through get_tool_config().

test_plan: TestPlanConfig

The [tool.click-extra.test-plan] sub-table (file/inline/timeout).

prebake: PrebakeConfig

The [tool.click-extra.prebake] sub-table (target module).

class click_extra.config.ConfigFormat(*values)[source]

Bases: Enum

All configuration formats, associated to their support status.

The first element of the tuple is a sequence of file extensions associated to the format. Patterns are fed to wcmatch.glob for matching, and are influenced by the flags set on the ConfigOption instance.

The second element indicates whether the format is supported or not, depending on the availability of the required third-party packages. This evaluation is performed at runtime when this module is imported.

Caution

The order is important for both format members and file patterns. It defines the priority order in which formats are tried when multiple candidate files are found.

Todo

Add support for JWCC / hujson format?

TOML = (('*.toml',), True, 'TOML')
YAML = (('*.yaml', '*.yml'), True, 'YAML')
JSON = (('*.json',), True, 'JSON')
JSON5 = (('*.json5',), True, 'JSON5')
JSONC = (('*.jsonc',), True, 'JSONC')
HJSON = (('*.hjson',), True, 'Hjson')
INI = (('*.ini',), True, 'INI')
XML = (('*.xml',), True, 'XML')
PYPROJECT_TOML = (('pyproject.toml',), True, 'pyproject.toml')
property label: str

Human-friendly name of the format for display in messages.

property enabled: bool

Returns True if the format is supported, False otherwise.

property patterns: tuple[str, ...]

Returns the default file patterns associated to the format.

class click_extra.config.ConfigOption(param_decls=None, metavar='CONFIG_PATH', type=UNPROCESSED, help='Location of the configuration file. Supports local path with glob patterns or remote URL.', is_eager=True, expose_value=False, file_format_patterns=None, file_pattern_flags=4104, roaming=True, force_posix=False, search_pattern_flags=285504, search_parents=False, stop_at=Sentinel.VCS, excluded_params=None, included_params=None, strict=False, config_schema=None, schema_strict=False, fallback_sections=(), config_validators=(), **kwargs)[source]

Bases: ExtraOption, ParamStructure

A pre-configured option adding --config CONFIG_PATH.

Takes as input a path to a file or folder, a glob pattern, or an URL.

  • is_eager is active by default so the callback gets the opportunity to set the default_map of the CLI before any other parameter is processed.

  • default is set to the value returned by self.default_pattern(), which is a pattern combining the default configuration folder for the CLI (as returned by click.get_app_dir()) and all supported file formats.

    Attention

    Default search pattern must follow the syntax of wcmatch.glob.

  • excluded_params are parameters which, if present in the configuration file, will be ignored and not applied to the CLI. Items are expected to be the fully-qualified ID of the parameter, as produced in the output of --show-params. Will default to the value of DEFAULT_EXCLUDED_PARAMS.

  • included_params is the inverse of excluded_params: only the listed parameters will be loaded from the configuration file. Cannot be used together with excluded_params.

file_format_patterns: dict[ConfigFormat, tuple[str, ...]]

Mapping of ConfigFormat to their associated file patterns.

Can be a string or a sequence of strings. This defines which configuration file formats are supported, and which file patterns are used to search for them.

Note

All formats depending on third-party dependencies that are not installed will be ignored.

Attention

File patterns must follow the syntax of wcmatch.fnmatch.

file_pattern_flags

Flags provided to all calls of wcmatch.fnmatch.

Applies to the matching of file names against supported format patterns specified in file_format_patterns.

Important

The SPLIT flag is always forced, as our multi-pattern design relies on it.

force_posix

Configuration for default folder search.

roaming and force_posix are fed to click.get_app_dir() to determine the location of the default configuration folder.

search_pattern_flags

Flags provided to all calls of wcmatch.glob.

Applies to both the default pattern and any user-provided pattern.

Important

The BRACE flag is always forced, so that multi-format default patterns using {pat1,pat2,...} syntax expand correctly.

The NODIR flag is always forced, to optimize the search for files only.

search_parents

Indicates whether to walk back the tree of parent folders when searching for configuration files.

stop_at

Boundary for parent directory walking.

  • None: walk up to filesystem root.

  • VCS: stop at the nearest VCS root (.git or .hg) (default).

  • A Path or str: stop at that directory.

included_params: frozenset[str] | None

Allowlist of parameter IDs, mutually exclusive with excluded_params.

None disables the allowlist. It is resolved into excluded_params by build_param_trees(), once every parameter ID is known.

strict

Defines the strictness of the configuration loading.

  • If True, raise an error if the configuration file contain parameters not recognized by the CLI.

  • If False, silently ignore unrecognized parameters.

config_schema

Optional schema for structured access to configuration values.

When set, the app’s configuration section is extracted from the parsed config file, normalized (hyphens replaced with underscores), flattened (nested dicts joined with _), and passed to this callable to produce a typed configuration object.

Supports:

  • Dataclass types: detected via __dataclass_fields__. Keys are normalized, nested dicts are flattened, and the result is filtered to known fields before instantiation. This allows nested config sections (like [tool.myapp.sub-section]) to map directly to flat dataclass fields (like sub_section_key).

  • Any callable dict T: called directly with the raw dict. Works with Pydantic’s Model.model_validate, attrs, or custom factory functions. The caller is responsible for key normalization and flattening.

The resulting object is stored in ctx.meta[click_extra.context.TOOL_CONFIG] and can be retrieved via get_tool_config.

schema_strict

Strictness for schema validation (separate from strict).

  • If True, raise ValueError when the config section contains keys that do not match any dataclass field (after normalization and flattening). Only applies when config_schema is a dataclass.

  • If False, silently ignore unrecognized keys.

Note

This is distinct from strict, which controls whether merge_default_map rejects config keys not matching CLI parameters. schema_strict validates against dataclass fields instead.

fallback_sections: Sequence[str]

Legacy section names to try when the app’s own section is empty.

Useful when a CLI tool has been renamed: old configuration files that still use [tool.old-name] (TOML), old-name: (YAML), or {"old-name": …} (JSON) are recognized with a deprecation warning. Works with all configuration formats.

config_validators: tuple[ConfigValidator, ...]

Extension validators for sub-trees of the configuration file.

Each ConfigValidator targets a dotted extension_path relative to the app section. Validators run after click-extra’s built-in CLI-parameter strict check (during --validate-config) and after the schema callable produces the typed configuration object (during normal config loading).

The list is seeded with click-extra’s built-in validators (currently the one for [tool.<cli>.themes.<name>] tables, see click_extra.theme.validate_themes_config()); user-supplied validators are appended after them. App code that registers its own validator on the same extension_path simply runs alongside the built-in: both validators are called, both sets of errors surface.

property excluded_params: frozenset[str][source]

Generates the default list of fully-qualified IDs to exclude.

Danger

It is only called once to produce the default exclusion list if the user did not provided its own.

It was not implemented in the constructor but made as a property, to allow for a just-in-time call within the current context. Without this trick we could not have fetched the CLI name.

property file_pattern: str[source]

Compile all file patterns from the supported formats.

Uses , (comma) notation to combine multiple patterns, suitable for wcmatch brace expansion ({pat1,pat2,...}).

Returns a single pattern string.

default_pattern()[source]

Returns the default pattern used to search for the configuration file.

Defaults to <app_dir>/{*.toml,*.json,*.ini}. Where <app_dir> is produced by the click.get_app_dir() method. The result depends on OS and is influenced by the roaming and force_posix properties.

Multiple file format patterns are wrapped with {…} brace-expansion syntax so that wcmatch.glob correctly applies the directory prefix to every sub-pattern.

Todo

Use platformdirs for more advanced configuration folder detection?

Return type:

str

get_help_extra(ctx)[source]

Replaces the default value of the configuration option.

Display a pretty path that is relative to the user’s home directory:

~/folder/my_cli/{*.toml,*.json,*.ini}

Instead of the full absolute path:

/home/user/folder/my_cli/{*.toml,*.json,*.ini}

Caution

This only applies when the GLOBTILDE flag is set in search_pattern_flags.

Return type:

OptionHelpExtra

parent_patterns(pattern)[source]

Generate (root_dir, file_pattern) pairs for searching.

Each yielded pair can be passed directly to glob.iglob(file_pattern, root_dir=root_dir) so that every sub-pattern (whether from BRACE or SPLIT expansion) is correctly scoped to the same directory.

root_dir is None for entirely magic patterns that will be evaluated relative to the current working directory.

Stops when reaching the root folder, the stop_at boundary, or an inaccessible directory.

Return type:

Iterable[tuple[str | None, str]]

search_and_read_file(pattern)[source]

Search filesystem or URL for files matching the pattern.

If pattern is an URL, download its content. A pattern is considered an URL only if it validates as one and starts with http:// or https://. All other patterns are considered glob patterns for local filesystem search.

Returns an iterator of the normalized location and its raw content, for each one matching the pattern. Only files are returned, directories are silently skipped.

This method returns the raw content of all matching patterns, without trying to parse them. If the content is empty, it is still returned as-is.

Also includes lookups into parents directories if self.search_parents is True.

Raises FileNotFoundError if no file was found after searching all locations.

Return type:

Iterable[tuple[Path | URL, str]]

parse_conf(content, formats)[source]

Parse the content with the given formats.

Tries to parse the given raw content string with each of the given formats, in order. Yields the resulting data structure for each successful parse.

Attention

Formats whose parsing raises an exception or does not return a dict are considered a failure and are skipped.

This follows the parse, don’t validate principle.

Return type:

Iterable[dict[str, Any] | None]

read_and_parse_conf(pattern)[source]

Search for a parseable configuration file.

Returns the location and data structure of the first configuration matching the pattern.

Only return the first match that:

  • exists,

  • is a file,

  • is not empty,

  • match file format patterns,

  • can be parsed successfully, and

  • produce a non-empty data structure.

Raises FileNotFoundError if no configuration file was found matching the criteria above.

Returns (None, None) if files were found but none could be parsed.

Return type:

tuple[Path | URL, dict[str, Any]] | tuple[None, None]

load_ini_config(content)[source]

Utility method to parse INI configuration file.

Internal convention is to use a dot (., as set by PARAM_PATH_SEP) in section IDs as a separator between levels. This is a workaround the limitation of INI format which doesn’t allow for sub-sections.

Returns a ready-to-use data structure.

Return type:

dict[str, Any]

merge_default_map(ctx, user_conf)[source]

Save the user configuration into the context’s default_map.

Merge the user configuration into the pre-computed template structure, which filters out all unrecognized options not supported by the command, then hand the result to _install_default_map().

Opaque sub-trees declared by the schema or by registered ConfigValidator instances are stripped from the conf before the CLI-parameter strict check, so user-controlled keys (like mappings whose keys are data, not flag names) don’t trip strict=True.

Note

This recomputes the filtered config that run_config_validation() already produces as merged_conf. load_conf() installs that result directly and skips this method; it stays as the standalone entry point for external callers.

Return type:

None

load_conf(ctx, param, path_pattern)[source]

Fetch parameter values from a configuration file and set them as defaults.

User configuration is merged to the context’s default_map, like Click does.

By relying on Click’s default_map, we make sure that precedence is respected. Direct CLI parameters, environment variables or interactive prompts take precedence over any values from the config file.

Hint

Once loading is complete, the resolved file path and its full parsed content are stored in ctx.meta[click_extra.context.CONF_SOURCE] and ctx.meta[click_extra.context.CONF_FULL] respectively. This is the recommended way to identify which configuration file was loaded.

We intentionally do not add a custom ParameterSource.CONFIG_FILE enum member: ParameterSource is a closed enum in Click, and monkeypatching it would be fragile. Besides, config values end up in default_map, so Click already reports them as ParameterSource.DEFAULT_MAP, which is accurate.

Return type:

None

class click_extra.config.ConfigValidator(extension_path, validator, description='')[source]

Bases: object

Register an app-defined extension validator for one sub-tree of the configuration file.

Apps register validators via the config_validators= kwarg on ConfigOption (or the matching decorator) to extend click-extra’s built-in CLI-parameter strict check with custom validation logic. Each validator targets a single dotted extension_path relative 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-config and 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 named my-cli with extension_path="managers" receives the contents of the [my-cli.managers] table.

  • validator (Callable[[dict[str, Any]], None]) – Callable taking the sub-tree dict and raising ValidationError on 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 --help text in a future release.

extension_path: str
validator: Callable[[dict[str, Any]], None]
description: str = ''
class click_extra.config.NoConfigOption(param_decls=None, type=UNPROCESSED, help='Ignore all configuration files and only use command line parameters and environment variables.', is_flag=True, flag_value=Sentinel.NO_CONFIG, is_eager=True, expose_value=False, **kwargs)[source]

Bases: ExtraOption

A pre-configured option adding --no-config.

This option is supposed to be used alongside the --config option (ConfigOption) to allow users to explicitly disable the use of any configuration file.

This is especially useful to debug side-effects caused by autodetection of configuration files.

flag_value=NO_CONFIG is the Sentinel enum member that signals “skip configuration loading” to ConfigOption. Click 8.4.0 (PR pallets/click#3363) auto-detects type=UNPROCESSED for non-basic flag_value types, but click-extra still supports Click 8.3.x where that auto-detection is absent, so the type=UNPROCESSED override is kept explicit to let the sentinel pass through Option unchanged on every supported Click.

See also

An alternative implementation of this class would be to create a custom click.ParamType instead of a custom Option subclass. Here is for example.

check_sibling_config_option(ctx, param, value)[source]

Ensure that this option is used alongside a ConfigOption instance.

Return type:

None

class click_extra.config.PrebakeConfig(module=None)[source]

Bases: object

Config schema for the prebake commands, read from [tool.<cli>.prebake].

Lets a project pin the target __init__.py once for its build pipeline, instead of passing --module to every click-extra prebake command.

module: str | None = None

Path to the __init__.py to pre-bake, resolved relative to the project root. Overrides the [project.scripts] auto-discovery; leave unset to keep it.

class click_extra.config.TestPlanConfig(file='./tests/cli-test-plan.yaml', inline=None, timeout=None)[source]

Bases: object

Config schema for a project’s test plan, read from [tool.<cli>.test-plan].

The test-plan CLI command resolves its cases from this config when no plan is given on the command line. Map it onto an app’s config section with a field carrying metadata={"click_extra.config_path": "test-plan"}.

file: str = './tests/cli-test-plan.yaml'

Path to a YAML test plan file, resolved relative to the project root.

inline: str | None = None

Inline YAML test plan, an alternative to file. Takes precedence.

timeout: int | None = None

Default timeout (seconds) for each case that does not set its own.

None leaves cases unbounded unless --timeout is passed.

class click_extra.config.ValidateConfigOption(param_decls=None, type=<click.types.Path object>, is_eager=True, expose_value=False, help='Validate the configuration file and exit.', **kwargs)[source]

Bases: ExtraOption

A pre-configured option adding --validate-config CONFIG_PATH.

Loads the config file at the given path, validates it against the CLI’s parameter structure in strict mode, reports results, and exits.

validate_config(ctx, param, value)[source]

Load, parse, and validate the configuration file, then exit.

Validation runs three checks in order, every one of them under the same ValidationError shape so the reported path is always rooted at the configuration file:

  1. CLI-parameter strict check on the non-opaque part of the document.

  2. Schema processing, if a config_schema is configured: catches type errors and unknown keys inside the dataclass-described section.

  3. Each registered ConfigValidator runs against its declared opaque sub-tree.

Every detected error is emitted before exiting, so a single --validate-config run surfaces the full list of fixes the user needs to apply.

Return type:

None

exception click_extra.config.ValidationError(path, message, code=None)[source]

Bases: Exception

Raised when a configuration file fails validation.

A single, structured exception type that uniformly carries the dotted path of the offending key, a human-readable message, and an optional code for programmatic handling. Used by click-extra’s built-in strict-mode check and by every user-registered ConfigValidator, so downstream apps and --validate-config see 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.ValidationReport(schema_instance, opaque_subtrees, errors, merged_conf=None)[source]

Bases: object

Outcome 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.

None when no schema is configured, or when the schema stage raised (in which case the failure is recorded in errors).

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 ValidationError detected, in stage order (unknown CLI-flag keys first, then schema errors, then validator failures). Empty on success.

With collect_all=False this 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’s default_map.

None when params_template was None (no strict check) or the strict check raised. Read it only on a successful report: it is the same value merge_default_map() would recompute, so reusing it avoids a second normalize/strip/merge pass.

property ok: bool

True when no error was detected.

click_extra.config.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:
  • conf (dict[str, Any]) – Nested dictionary to flatten.

  • sep (str) – Separator used to join parent and child keys. Defaults to "_" which produces valid Python identifiers when combined with normalize_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 as dict[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:

dict[str, Any]

click_extra.config.get_tool_config(ctx=None)[source]

Retrieve the typed tool configuration from the context.

Returns the object stored under click_extra.context.TOOL_CONFIG by ConfigOption when a config_schema is set, or None if no schema was configured or no configuration was loaded.

Parameters:

ctx (Context | None) – Click context. Defaults to the current context.

Return type:

Any

click_extra.config.make_schema_callable(schema, *, strict=False, normalize=True)[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:

    1. Type-aware flattening. Fields typed as dict[str, X] are treated as opaque: flatten_config_keys stops at their boundary so the dict value is kept intact.

    2. Field metadata. Dataclass fields may carry click_extra.config_path (a dotted TOML path like "test-matrix.replace") and click_extra.normalize_keys (False to skip key normalization on the extracted value). Fields with an explicit path are extracted from the raw config before normalization and flattening.

    3. 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.

  • None returns None.

Parameters:
  • strict (bool) – If True, raise ValueError when the config contains keys that do not match any dataclass field (after normalization and flattening).

  • normalize (bool) – If False, skip normalize_config_keys on the remaining config dict. Used internally when recursing into nested dataclasses whose parent opted out of normalization via click_extra.normalize_keys = False.

Return type:

Callable[[dict[str, Any]], Any] | None

click_extra.config.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-bar becomes foo_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 with flatten_config_keys’s opaque_keys to 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_name helper, so downstream projects like Click Extra can reuse it instead of duplicating the transform.

Return type:

dict[str, Any]

click_extra.config.run_config_validation(user_conf, *, app_name, params_template, config_schema=None, config_validators=(), fallback_sections=(), schema_strict=False, strict=False, 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 ConfigValidator hooks) behind a single function yielding a single error type. It is deliberately not named validate_config: that name belongs to validate_config(), the callback powering the --validate-config flag.

Stages, in order:

  1. Normalize. Strip reserved keys and expand dotted keys.

  2. 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 in ValidationReport.opaque_subtrees.

  3. Strict-check the CLI-flag-bound part against params_template, keeping the merged result as ValidationReport.merged_conf (skipped when params_template is None).

  4. Schema-build the app section through the configured callable, producing ValidationReport.schema_instance.

  5. 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. Pass None to 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, or None.

  • config_validators (Sequence[ConfigValidator]) – Extension validators to run against opaque sub-trees.

  • fallback_sections (Sequence[str]) – Legacy section names to try when app_name is absent or empty.

  • schema_strict (bool) – Reject keys the dataclass schema does not recognize.

  • strict (bool) – Reject keys the CLI-parameter template does not recognize.

  • collect_all (bool) – When True (default), run every stage and collect all errors. When False, the first error short-circuits the rest.

Return type:

ValidationReport

Returns:

A ValidationReport. ValidationError is the single error type recorded by every stage; ValueError / TypeError raised by the strict check or schema callable are wrapped into it.

Submodules

click_extra.config.formats module

Configuration file formats and their stateless content parsers.

Holds the ConfigFormat enum, the optional third-party parser probes that decide which formats are enabled, and parse_content(), the stateless dispatch used by ConfigOption for every format that does not need the CLI parameter structure.

click_extra.config.formats.PARSER_SUPPORT: dict[str, bool] = {'hjson': True, 'json5': True, 'jsonc': True, 'xml': True, 'yaml': True}

Availability of each optional parser, keyed by click-extra[extra] name.

Populated once at import time by probing each module in _OPTIONAL_PARSERS with importlib.util.find_spec(). Read by ConfigFormat to mark the matching format as enabled or disabled. The probe does not import the module, so the actual parser is loaded lazily by parse_content() only when used.

class click_extra.config.formats.ConfigFormat(*values)[source]

Bases: Enum

All configuration formats, associated to their support status.

The first element of the tuple is a sequence of file extensions associated to the format. Patterns are fed to wcmatch.glob for matching, and are influenced by the flags set on the ConfigOption instance.

The second element indicates whether the format is supported or not, depending on the availability of the required third-party packages. This evaluation is performed at runtime when this module is imported.

Caution

The order is important for both format members and file patterns. It defines the priority order in which formats are tried when multiple candidate files are found.

Todo

Add support for JWCC / hujson format?

TOML = (('*.toml',), True, 'TOML')
YAML = (('*.yaml', '*.yml'), True, 'YAML')
JSON = (('*.json',), True, 'JSON')
JSON5 = (('*.json5',), True, 'JSON5')
JSONC = (('*.jsonc',), True, 'JSONC')
HJSON = (('*.hjson',), True, 'Hjson')
INI = (('*.ini',), True, 'INI')
XML = (('*.xml',), True, 'XML')
PYPROJECT_TOML = (('pyproject.toml',), True, 'pyproject.toml')
property label: str

Human-friendly name of the format for display in messages.

property enabled: bool

Returns True if the format is supported, False otherwise.

property patterns: tuple[str, ...]

Returns the default file patterns associated to the format.

click_extra.config.formats.parse_content(fmt, content)[source]

Parse content with a single stateless format.

INI is excluded: it needs the CLI parameter structure for type coercion and is handled by ConfigOption.load_ini_config.

Note

Optional third-party parsers are imported lazily, at the point of use, rather than at module load. Only enabled formats reach this function (disabled ones are filtered out of ConfigOption.file_format_patterns), so the import always resolves for the formats actually parsed here.

Return type:

Any

click_extra.config.option module

Utilities to load parameters and options from a configuration file.

Hint

Why config?

That whole namespace is using the common config short-name to designate configuration files.

Not conf, not cfg, not configuration, not settings. Just config.

A quick survey of existing practices, and poll to my friends informed me that config is more explicit and less likely to be misunderstood.

After all, is there a chance for it to be misunderstood, in the context of a CLI, for something else? Confirm? Conference? Conflict Confuse?…

So yes, config is good enough.

Todo

Add a --dump-config or --export-config option to write down the current configuration (or a template) into a file or <stdout>.

Help message would be: you can use this option with other options or environment variables to have them set in the generated configuration.

Dotted keys in configuration files (like "subcommand.option": value) are automatically expanded into nested dicts before merging, so users can freely mix flat dot-notation and nested structures in any supported format.

click_extra.config.option.VCS_DIRS = ('.git', '.hg', '.svn', '.bzr', 'CVS', '.darcs')

VCS directory names used to identify version control system roots.

Includes: - .git: Git - .hg: Mercurial - .svn: Subversion - .bzr: Bazaar - CVS: CVS (note: uppercase, no leading dot) - .darcs: Darcs

click_extra.config.option.CONFIG_OPTION_NAME = 'config'

Hardcoded name of the configuration option.

This name is going to be shared by both the --config and --no-config options below, so they can compete with each other to either set a path pattern or disable the use of any configuration file at all.

click_extra.config.option.DEFAULT_EXCLUDED_PARAMS = ('config', 'help', 'show_params', 'version')

Default parameter IDs to exclude from the configuration file.

Defaults to:

  • --config option, which cannot be used to recursively load another configuration file.

  • --help, as it makes no sense to have the configurable file always forces a CLI to show the help and exit.

  • --show-params flag, which is like --help and stops the CLI execution.

  • --version, which is not a configurable option per-se.

class click_extra.config.option.Sentinel(*values)[source]

Bases: Enum

Enum used to define sentinel values.

Note

This reuse the same pattern as Click._utils.Sentinel.

NO_CONFIG = <object object>
VCS = <object object>
click_extra.config.option.NO_CONFIG = Sentinel.NO_CONFIG

Sentinel used to indicate that no configuration file must be used at all.

click_extra.config.option.VCS = Sentinel.VCS

Sentinel used to stop parent directory walking at the nearest VCS root.

class click_extra.config.option.ConfigOption(param_decls=None, metavar='CONFIG_PATH', type=UNPROCESSED, help='Location of the configuration file. Supports local path with glob patterns or remote URL.', is_eager=True, expose_value=False, file_format_patterns=None, file_pattern_flags=4104, roaming=True, force_posix=False, search_pattern_flags=285504, search_parents=False, stop_at=Sentinel.VCS, excluded_params=None, included_params=None, strict=False, config_schema=None, schema_strict=False, fallback_sections=(), config_validators=(), **kwargs)[source]

Bases: ExtraOption, ParamStructure

A pre-configured option adding --config CONFIG_PATH.

Takes as input a path to a file or folder, a glob pattern, or an URL.

  • is_eager is active by default so the callback gets the opportunity to set the default_map of the CLI before any other parameter is processed.

  • default is set to the value returned by self.default_pattern(), which is a pattern combining the default configuration folder for the CLI (as returned by click.get_app_dir()) and all supported file formats.

    Attention

    Default search pattern must follow the syntax of wcmatch.glob.

  • excluded_params are parameters which, if present in the configuration file, will be ignored and not applied to the CLI. Items are expected to be the fully-qualified ID of the parameter, as produced in the output of --show-params. Will default to the value of DEFAULT_EXCLUDED_PARAMS.

  • included_params is the inverse of excluded_params: only the listed parameters will be loaded from the configuration file. Cannot be used together with excluded_params.

file_format_patterns: dict[ConfigFormat, tuple[str, ...]]

Mapping of ConfigFormat to their associated file patterns.

Can be a string or a sequence of strings. This defines which configuration file formats are supported, and which file patterns are used to search for them.

Note

All formats depending on third-party dependencies that are not installed will be ignored.

Attention

File patterns must follow the syntax of wcmatch.fnmatch.

file_pattern_flags

Flags provided to all calls of wcmatch.fnmatch.

Applies to the matching of file names against supported format patterns specified in file_format_patterns.

Important

The SPLIT flag is always forced, as our multi-pattern design relies on it.

force_posix

Configuration for default folder search.

roaming and force_posix are fed to click.get_app_dir() to determine the location of the default configuration folder.

search_pattern_flags

Flags provided to all calls of wcmatch.glob.

Applies to both the default pattern and any user-provided pattern.

Important

The BRACE flag is always forced, so that multi-format default patterns using {pat1,pat2,...} syntax expand correctly.

The NODIR flag is always forced, to optimize the search for files only.

search_parents

Indicates whether to walk back the tree of parent folders when searching for configuration files.

stop_at

Boundary for parent directory walking.

  • None: walk up to filesystem root.

  • VCS: stop at the nearest VCS root (.git or .hg) (default).

  • A Path or str: stop at that directory.

included_params: frozenset[str] | None

Allowlist of parameter IDs, mutually exclusive with excluded_params.

None disables the allowlist. It is resolved into excluded_params by build_param_trees(), once every parameter ID is known.

strict

Defines the strictness of the configuration loading.

  • If True, raise an error if the configuration file contain parameters not recognized by the CLI.

  • If False, silently ignore unrecognized parameters.

config_schema

Optional schema for structured access to configuration values.

When set, the app’s configuration section is extracted from the parsed config file, normalized (hyphens replaced with underscores), flattened (nested dicts joined with _), and passed to this callable to produce a typed configuration object.

Supports:

  • Dataclass types: detected via __dataclass_fields__. Keys are normalized, nested dicts are flattened, and the result is filtered to known fields before instantiation. This allows nested config sections (like [tool.myapp.sub-section]) to map directly to flat dataclass fields (like sub_section_key).

  • Any callable dict T: called directly with the raw dict. Works with Pydantic’s Model.model_validate, attrs, or custom factory functions. The caller is responsible for key normalization and flattening.

The resulting object is stored in ctx.meta[click_extra.context.TOOL_CONFIG] and can be retrieved via get_tool_config.

schema_strict

Strictness for schema validation (separate from strict).

  • If True, raise ValueError when the config section contains keys that do not match any dataclass field (after normalization and flattening). Only applies when config_schema is a dataclass.

  • If False, silently ignore unrecognized keys.

Note

This is distinct from strict, which controls whether merge_default_map rejects config keys not matching CLI parameters. schema_strict validates against dataclass fields instead.

fallback_sections: Sequence[str]

Legacy section names to try when the app’s own section is empty.

Useful when a CLI tool has been renamed: old configuration files that still use [tool.old-name] (TOML), old-name: (YAML), or {"old-name": …} (JSON) are recognized with a deprecation warning. Works with all configuration formats.

config_validators: tuple[ConfigValidator, ...]

Extension validators for sub-trees of the configuration file.

Each ConfigValidator targets a dotted extension_path relative to the app section. Validators run after click-extra’s built-in CLI-parameter strict check (during --validate-config) and after the schema callable produces the typed configuration object (during normal config loading).

The list is seeded with click-extra’s built-in validators (currently the one for [tool.<cli>.themes.<name>] tables, see click_extra.theme.validate_themes_config()); user-supplied validators are appended after them. App code that registers its own validator on the same extension_path simply runs alongside the built-in: both validators are called, both sets of errors surface.

property excluded_params: frozenset[str][source]

Generates the default list of fully-qualified IDs to exclude.

Danger

It is only called once to produce the default exclusion list if the user did not provided its own.

It was not implemented in the constructor but made as a property, to allow for a just-in-time call within the current context. Without this trick we could not have fetched the CLI name.

property file_pattern: str[source]

Compile all file patterns from the supported formats.

Uses , (comma) notation to combine multiple patterns, suitable for wcmatch brace expansion ({pat1,pat2,...}).

Returns a single pattern string.

default_pattern()[source]

Returns the default pattern used to search for the configuration file.

Defaults to <app_dir>/{*.toml,*.json,*.ini}. Where <app_dir> is produced by the click.get_app_dir() method. The result depends on OS and is influenced by the roaming and force_posix properties.

Multiple file format patterns are wrapped with {…} brace-expansion syntax so that wcmatch.glob correctly applies the directory prefix to every sub-pattern.

Todo

Use platformdirs for more advanced configuration folder detection?

Return type:

str

get_help_extra(ctx)[source]

Replaces the default value of the configuration option.

Display a pretty path that is relative to the user’s home directory:

~/folder/my_cli/{*.toml,*.json,*.ini}

Instead of the full absolute path:

/home/user/folder/my_cli/{*.toml,*.json,*.ini}

Caution

This only applies when the GLOBTILDE flag is set in search_pattern_flags.

Return type:

OptionHelpExtra

parent_patterns(pattern)[source]

Generate (root_dir, file_pattern) pairs for searching.

Each yielded pair can be passed directly to glob.iglob(file_pattern, root_dir=root_dir) so that every sub-pattern (whether from BRACE or SPLIT expansion) is correctly scoped to the same directory.

root_dir is None for entirely magic patterns that will be evaluated relative to the current working directory.

Stops when reaching the root folder, the stop_at boundary, or an inaccessible directory.

Return type:

Iterable[tuple[str | None, str]]

search_and_read_file(pattern)[source]

Search filesystem or URL for files matching the pattern.

If pattern is an URL, download its content. A pattern is considered an URL only if it validates as one and starts with http:// or https://. All other patterns are considered glob patterns for local filesystem search.

Returns an iterator of the normalized location and its raw content, for each one matching the pattern. Only files are returned, directories are silently skipped.

This method returns the raw content of all matching patterns, without trying to parse them. If the content is empty, it is still returned as-is.

Also includes lookups into parents directories if self.search_parents is True.

Raises FileNotFoundError if no file was found after searching all locations.

Return type:

Iterable[tuple[Path | URL, str]]

parse_conf(content, formats)[source]

Parse the content with the given formats.

Tries to parse the given raw content string with each of the given formats, in order. Yields the resulting data structure for each successful parse.

Attention

Formats whose parsing raises an exception or does not return a dict are considered a failure and are skipped.

This follows the parse, don’t validate principle.

Return type:

Iterable[dict[str, Any] | None]

read_and_parse_conf(pattern)[source]

Search for a parseable configuration file.

Returns the location and data structure of the first configuration matching the pattern.

Only return the first match that:

  • exists,

  • is a file,

  • is not empty,

  • match file format patterns,

  • can be parsed successfully, and

  • produce a non-empty data structure.

Raises FileNotFoundError if no configuration file was found matching the criteria above.

Returns (None, None) if files were found but none could be parsed.

Return type:

tuple[Path | URL, dict[str, Any]] | tuple[None, None]

load_ini_config(content)[source]

Utility method to parse INI configuration file.

Internal convention is to use a dot (., as set by PARAM_PATH_SEP) in section IDs as a separator between levels. This is a workaround the limitation of INI format which doesn’t allow for sub-sections.

Returns a ready-to-use data structure.

Return type:

dict[str, Any]

merge_default_map(ctx, user_conf)[source]

Save the user configuration into the context’s default_map.

Merge the user configuration into the pre-computed template structure, which filters out all unrecognized options not supported by the command, then hand the result to _install_default_map().

Opaque sub-trees declared by the schema or by registered ConfigValidator instances are stripped from the conf before the CLI-parameter strict check, so user-controlled keys (like mappings whose keys are data, not flag names) don’t trip strict=True.

Note

This recomputes the filtered config that run_config_validation() already produces as merged_conf. load_conf() installs that result directly and skips this method; it stays as the standalone entry point for external callers.

Return type:

None

load_conf(ctx, param, path_pattern)[source]

Fetch parameter values from a configuration file and set them as defaults.

User configuration is merged to the context’s default_map, like Click does.

By relying on Click’s default_map, we make sure that precedence is respected. Direct CLI parameters, environment variables or interactive prompts take precedence over any values from the config file.

Hint

Once loading is complete, the resolved file path and its full parsed content are stored in ctx.meta[click_extra.context.CONF_SOURCE] and ctx.meta[click_extra.context.CONF_FULL] respectively. This is the recommended way to identify which configuration file was loaded.

We intentionally do not add a custom ParameterSource.CONFIG_FILE enum member: ParameterSource is a closed enum in Click, and monkeypatching it would be fragile. Besides, config values end up in default_map, so Click already reports them as ParameterSource.DEFAULT_MAP, which is accurate.

Return type:

None

class click_extra.config.option.NoConfigOption(param_decls=None, type=UNPROCESSED, help='Ignore all configuration files and only use command line parameters and environment variables.', is_flag=True, flag_value=Sentinel.NO_CONFIG, is_eager=True, expose_value=False, **kwargs)[source]

Bases: ExtraOption

A pre-configured option adding --no-config.

This option is supposed to be used alongside the --config option (ConfigOption) to allow users to explicitly disable the use of any configuration file.

This is especially useful to debug side-effects caused by autodetection of configuration files.

flag_value=NO_CONFIG is the Sentinel enum member that signals “skip configuration loading” to ConfigOption. Click 8.4.0 (PR pallets/click#3363) auto-detects type=UNPROCESSED for non-basic flag_value types, but click-extra still supports Click 8.3.x where that auto-detection is absent, so the type=UNPROCESSED override is kept explicit to let the sentinel pass through Option unchanged on every supported Click.

See also

An alternative implementation of this class would be to create a custom click.ParamType instead of a custom Option subclass. Here is for example.

check_sibling_config_option(ctx, param, value)[source]

Ensure that this option is used alongside a ConfigOption instance.

Return type:

None

class click_extra.config.option.ValidateConfigOption(param_decls=None, type=<click.types.Path object>, is_eager=True, expose_value=False, help='Validate the configuration file and exit.', **kwargs)[source]

Bases: ExtraOption

A pre-configured option adding --validate-config CONFIG_PATH.

Loads the config file at the given path, validates it against the CLI’s parameter structure in strict mode, reports results, and exits.

validate_config(ctx, param, value)[source]

Load, parse, and validate the configuration file, then exit.

Validation runs three checks in order, every one of them under the same ValidationError shape so the reported path is always rooted at the configuration file:

  1. CLI-parameter strict check on the non-opaque part of the document.

  2. Schema processing, if a config_schema is configured: catches type errors and unknown keys inside the dataclass-described section.

  3. Each registered ConfigValidator runs against its declared opaque sub-tree.

Every detected error is emitted before exiting, so a single --validate-config run surfaces the full list of fixes the user needs to apply.

Return type:

None

class click_extra.config.option.TestPlanConfig(file='./tests/cli-test-plan.yaml', inline=None, timeout=None)[source]

Bases: object

Config schema for a project’s test plan, read from [tool.<cli>.test-plan].

The test-plan CLI command resolves its cases from this config when no plan is given on the command line. Map it onto an app’s config section with a field carrying metadata={"click_extra.config_path": "test-plan"}.

file: str = './tests/cli-test-plan.yaml'

Path to a YAML test plan file, resolved relative to the project root.

inline: str | None = None

Inline YAML test plan, an alternative to file. Takes precedence.

timeout: int | None = None

Default timeout (seconds) for each case that does not set its own.

None leaves cases unbounded unless --timeout is passed.

class click_extra.config.option.PrebakeConfig(module=None)[source]

Bases: object

Config schema for the prebake commands, read from [tool.<cli>.prebake].

Lets a project pin the target __init__.py once for its build pipeline, instead of passing --module to every click-extra prebake command.

module: str | None = None

Path to the __init__.py to pre-bake, resolved relative to the project root. Overrides the [project.scripts] auto-discovery; leave unset to keep it.

class click_extra.config.option.ClickExtraConfig(test_plan=<factory>, prebake=<factory>)[source]

Bases: object

Schema for the [tool.click-extra] configuration section.

Wired as the config_schema of the top-level click-extra group, so every subcommand reads the same section and pulls its own sub-table through get_tool_config().

test_plan: TestPlanConfig

The [tool.click-extra.test-plan] sub-table (file/inline/timeout).

prebake: PrebakeConfig

The [tool.click-extra.prebake] sub-table (target module).

click_extra.config.schema module

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_subcommands which only fires when no subcommands are given on the CLI, _prepend_subcommands always prepends the listed subcommands. This is useful for always injecting a debug subcommand on a dev machine, for example.

Only works with chain=True groups (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 as dict[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 a dict (for example, a nested dataclass that nonetheless represents user-extensible content).

click_extra.config.schema.THEMES_CONFIG_KEY: str = 'themes'

Sub-key under [tool.<cli>] where user-defined themes live in config.

Used by ConfigOption to find [tool.<cli>.themes.<name>] tables, build them via HelpTheme.from_dict, and stash the result on ctx.meta[click_extra.context.THEME_OVERRIDES]. The constant is the single source of truth shared by _builtin_config_validators, ConfigOption._apply_theme_overrides, and click_extra.theme.themes_from_config().

exception click_extra.config.schema.ValidationError(path, message, code=None)[source]

Bases: Exception

Raised when a configuration file fails validation.

A single, structured exception type that uniformly carries the dotted path of the offending key, a human-readable message, and an optional code for programmatic handling. Used by click-extra’s built-in strict-mode check and by every user-registered ConfigValidator, so downstream apps and --validate-config see 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: object

Register an app-defined extension validator for one sub-tree of the configuration file.

Apps register validators via the config_validators= kwarg on ConfigOption (or the matching decorator) to extend click-extra’s built-in CLI-parameter strict check with custom validation logic. Each validator targets a single dotted extension_path relative 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-config and 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 named my-cli with extension_path="managers" receives the contents of the [my-cli.managers] table.

  • validator (Callable[[dict[str, Any]], None]) – Callable taking the sub-tree dict and raising ValidationError on 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 --help text in a future release.

extension_path: str
validator: Callable[[dict[str, Any]], None]
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-bar becomes foo_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 with flatten_config_keys’s opaque_keys to 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_name helper, so downstream projects like Click Extra can reuse it instead of duplicating the transform.

Return type:

dict[str, Any]

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:
  • conf (dict[str, Any]) – Nested dictionary to flatten.

  • sep (str) – Separator used to join parent and child keys. Defaults to "_" which produces valid Python identifiers when combined with normalize_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 as dict[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:

dict[str, Any]

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_CONFIG by ConfigOption when a config_schema is set, or None if no schema was configured or no configuration was loaded.

Parameters:

ctx (Context | None) – Click context. Defaults to the current context.

Return type:

Any

click_extra.config.schema.make_schema_callable(schema, *, strict=False, normalize=True)[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:

    1. Type-aware flattening. Fields typed as dict[str, X] are treated as opaque: flatten_config_keys stops at their boundary so the dict value is kept intact.

    2. Field metadata. Dataclass fields may carry click_extra.config_path (a dotted TOML path like "test-matrix.replace") and click_extra.normalize_keys (False to skip key normalization on the extracted value). Fields with an explicit path are extracted from the raw config before normalization and flattening.

    3. 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.

  • None returns None.

Parameters:
  • strict (bool) – If True, raise ValueError when the config contains keys that do not match any dataclass field (after normalization and flattening).

  • normalize (bool) – If False, skip normalize_config_keys on the remaining config dict. Used internally when recursing into nested dataclasses whose parent opted out of normalization via click_extra.normalize_keys = False.

Return type:

Callable[[dict[str, Any]], Any] | None

class click_extra.config.schema.ValidationReport(schema_instance, opaque_subtrees, errors, merged_conf=None)[source]

Bases: object

Outcome 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.

None when no schema is configured, or when the schema stage raised (in which case the failure is recorded in errors).

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 ValidationError detected, in stage order (unknown CLI-flag keys first, then schema errors, then validator failures). Empty on success.

With collect_all=False this 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’s default_map.

None when params_template was None (no strict check) or the strict check raised. Read it only on a successful report: it is the same value merge_default_map() would recompute, so reusing it avoids a second normalize/strip/merge pass.

property ok: bool

True when 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, strict=False, 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 ConfigValidator hooks) behind a single function yielding a single error type. It is deliberately not named validate_config: that name belongs to validate_config(), the callback powering the --validate-config flag.

Stages, in order:

  1. Normalize. Strip reserved keys and expand dotted keys.

  2. 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 in ValidationReport.opaque_subtrees.

  3. Strict-check the CLI-flag-bound part against params_template, keeping the merged result as ValidationReport.merged_conf (skipped when params_template is None).

  4. Schema-build the app section through the configured callable, producing ValidationReport.schema_instance.

  5. 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. Pass None to 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, or None.

  • config_validators (Sequence[ConfigValidator]) – Extension validators to run against opaque sub-trees.

  • fallback_sections (Sequence[str]) – Legacy section names to try when app_name is absent or empty.

  • schema_strict (bool) – Reject keys the dataclass schema does not recognize.

  • strict (bool) – Reject keys the CLI-parameter template does not recognize.

  • collect_all (bool) – When True (default), run every stage and collect all errors. When False, the first error short-circuits the rest.

Return type:

ValidationReport

Returns:

A ValidationReport. ValidationError is the single error type recorded by every stage; ValueError / TypeError raised by the strict check or schema callable are wrapped into it.