From c4490fe722e88b08adeed8d8634496964f25171f Mon Sep 17 00:00:00 2001 From: Zach Date: Sun, 24 May 2026 01:40:28 -0600 Subject: [PATCH] feat(config): add `--describe` flag to `pixi config list` Adds an annotated TOML-style listing of every available configuration option with its type, default, and a short explanation. Keys that are currently set print uncommented with their value; the rest are emitted as commented stubs showing the default. `--describe [KEY]` describes one key, including per-bucket keys such as `s3-options.my-bucket.region`. `--describe --json` emits a structured array suitable for tooling. The option is also documented in the configuration reference. Closes #1457. --- crates/pixi_cli/src/config.rs | 183 +++++++++ crates/pixi_config/src/lib.rs | 373 +++++++++++++++++++ docs/reference/cli/pixi/config/list.md | 2 + docs/reference/cli/pixi/config/list_extender | 2 + docs/reference/pixi_configuration.md | 13 + tests/integration_python/test_main_cli.py | 144 +++++++ 6 files changed, 717 insertions(+) diff --git a/crates/pixi_cli/src/config.rs b/crates/pixi_cli/src/config.rs index a43fbe5a53..59bf9552c2 100644 --- a/crates/pixi_cli/src/config.rs +++ b/crates/pixi_cli/src/config.rs @@ -90,6 +90,10 @@ struct ListArgs { #[arg(long)] json: bool, + /// Describe every configuration option with its type, default, and a short explanation + #[arg(long)] + describe: bool, + #[clap(flatten)] config_source: pixi_config::ConfigSourceCli, @@ -214,6 +218,13 @@ pub async fn execute(args: Args) -> miette::Result<()> { Subcommand::List(args) => { let config = load_config(&args.common, &args.config_source.source())?; + if args.describe { + let out = render_describe(&config, args.key.as_deref(), args.json)?; + pixi_utils::io::ignore_broken_pipe(writeln!(std::io::stdout(), "{out}")) + .into_diagnostic()?; + return Ok(()); + } + let out = if let Some(key) = args.key { let partial = partial_config(&config, &key)?; if args.json { @@ -696,6 +707,176 @@ fn resolve_parent_keys(doc: &TomlDocument, parents: &[&str]) -> Vec { resolved } +fn render_describe( + config: &Config, + key_filter: Option<&str>, + json: bool, +) -> miette::Result { + let descriptions = config.describe_keys(); + let as_listed = |opt: &'static pixi_config::ConfigOptionDescription| { + // Keys with a placeholder segment have no single value to show. + let path = + (!opt.key.contains('<')).then(|| opt.key.split('.').map(str::to_string).collect()); + (opt, opt.key.to_string(), path) + }; + + // Each selected option is paired with the name to display and the path to + // look its value up at. A filter like `s3-options.my-bucket.region` matches + // the `s3-options..region` option, like `pixi config list `. + let selected: Vec<( + &pixi_config::ConfigOptionDescription, + String, + Option>, + )> = if let Some(k) = key_filter { + if let Some(opt) = descriptions.iter().find(|opt| opt.key == k) { + // Keys copied from the listing, placeholders included, match as-is. + vec![as_listed(opt)] + } else { + let key_path = KeyPath::parse(k)?; + let segments: Vec<&str> = key_path + .parents() + .into_iter() + .chain([key_path.target()]) + .collect(); + let matches: Vec<_> = descriptions + .iter() + .filter(|opt| is_known_key(&[opt.key], &segments)) + .map(|opt| { + let path = segments.iter().map(|s| s.to_string()).collect(); + (opt, k.to_string(), Some(path)) + }) + .collect(); + if matches.is_empty() { + return Err(miette::miette!( + "Unknown configuration key '{}'. Run `pixi config list --describe` to list every available key.", + k + )); + } + matches + } + } else { + descriptions.iter().map(as_listed).collect() + }; + + let doc = toml_edit::ser::to_string_pretty(config) + .into_diagnostic() + .and_then(|s| s.parse::().into_diagnostic())?; + let lookup = |path: &Option>| { + path.as_ref() + .and_then(|path| lookup_path(doc.as_table(), path)) + }; + + if json { + // Go through TOML like `partial_config` does, so unset fields are + // dropped instead of showing up as nulls. + let json_doc: serde_json::Value = + toml_edit::de::from_str(&doc.to_string()).into_diagnostic()?; + let arr: Vec = selected + .iter() + .map(|(opt, name, path)| { + let value = path + .as_ref() + .and_then(|path| { + path.iter() + .try_fold(&json_doc, |value, segment| value.get(segment)) + }) + .cloned() + .unwrap_or(serde_json::Value::Null); + serde_json::json!({ + "key": name, + "description": opt.description, + "type": opt.value_type, + "default": opt.default, + "value": value, + }) + }) + .collect(); + return serde_json::to_string_pretty(&arr).into_diagnostic(); + } + + let mut out = String::new(); + for (i, (opt, name, path)) in selected.iter().enumerate() { + if i > 0 { + out.push('\n'); + } + out.push_str("# "); + out.push_str(opt.description); + out.push('\n'); + out.push_str("# Type: "); + out.push_str(opt.value_type); + out.push('\n'); + out.push_str("# Default: "); + out.push_str(opt.default); + out.push('\n'); + + match lookup(path).map(format_toml_value) { + Some(value) => { + out.push_str(name); + out.push_str(" = "); + out.push_str(&value); + out.push('\n'); + } + None => { + out.push_str("# "); + out.push_str(name); + out.push_str(" = "); + out.push_str(opt.default); + out.push('\n'); + } + } + } + Ok(out) +} + +fn lookup_path<'a>(table: &'a toml_edit::Table, path: &[String]) -> Option<&'a toml_edit::Item> { + let (first, rest) = path.split_first()?; + let mut item = table.get(first)?; + for part in rest { + item = item.as_table_like().and_then(|t| t.get(part))?; + } + Some(item) +} + +fn format_toml_value(item: &toml_edit::Item) -> String { + match item { + toml_edit::Item::Value(v) => v.to_string().trim().to_string(), + toml_edit::Item::Table(t) => table_to_inline(t).to_string().trim().to_string(), + toml_edit::Item::ArrayOfTables(arr) => { + let mut out = toml_edit::Array::new(); + for t in arr { + out.push(table_to_inline(t)); + } + out.to_string().trim().to_string() + } + toml_edit::Item::None => String::new(), + } +} + +fn table_to_inline(table: &toml_edit::Table) -> toml_edit::InlineTable { + let mut inline = toml_edit::InlineTable::new(); + for (k, v) in table.iter() { + if let Some(val) = item_to_value(v) { + inline.insert(k, val); + } + } + inline +} + +fn item_to_value(item: &toml_edit::Item) -> Option { + match item { + toml_edit::Item::Value(v) => Some(v.clone()), + toml_edit::Item::Table(t) => Some(toml_edit::Value::InlineTable(table_to_inline(t))), + toml_edit::Item::ArrayOfTables(arr) => { + let mut out = toml_edit::Array::new(); + for t in arr { + out.push(table_to_inline(t)); + } + Some(toml_edit::Value::Array(out)) + } + toml_edit::Item::None => None, + } +} + /// Extract only the value at the (possibly nested) `key` from the config, /// wrapped in its parent tables so it serializes like the full config would. fn partial_config(config: &Config, key: &str) -> miette::Result { @@ -862,6 +1043,7 @@ mod tests { execute_subcommand(Subcommand::List(ListArgs { key: None, json: false, + describe: false, common: test_context.common_args, config_source: pixi_config::ConfigSourceCli::default(), })) @@ -1482,6 +1664,7 @@ region = "us-east-1" subcommand: Subcommand::List(ListArgs { key: Some(key.clone()), json: false, + describe: false, common: test_context.common_args.clone(), config_source: pixi_config::ConfigSourceCli { no_config: true, diff --git a/crates/pixi_config/src/lib.rs b/crates/pixi_config/src/lib.rs index e1ebe572b6..35ebe4da5f 100644 --- a/crates/pixi_config/src/lib.rs +++ b/crates/pixi_config/src/lib.rs @@ -26,6 +26,348 @@ use url::Url; const EXPERIMENTAL: &str = "experimental"; +/// User-facing metadata for a single configuration key. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ConfigOptionDescription { + pub key: &'static str, + pub description: &'static str, + pub value_type: &'static str, + pub default: &'static str, +} + +static CONFIG_OPTION_DESCRIPTIONS: &[ConfigOptionDescription] = &[ + ConfigOptionDescription { + key: "authentication-override-file", + description: "Path to the file containing the authentication token.", + value_type: "path", + default: "(unset)", + }, + ConfigOptionDescription { + key: "cache", + description: "Per-cache-directory overrides. See `cache.*` keys; can also be set as a JSON object.", + value_type: "table", + default: "(empty)", + }, + ConfigOptionDescription { + key: "cache.build-tool-environments", + description: "Cache directory for build-tool environments. Inherits from `cache.root` when unset.", + value_type: "path", + default: "(inherited)", + }, + ConfigOptionDescription { + key: "cache.conda-packages", + description: "Cache directory for the conda package store (`pkgs`). Inherits from `cache.root` when unset.", + value_type: "path", + default: "(inherited)", + }, + ConfigOptionDescription { + key: "cache.detached-environments", + description: "Root directory for detached environments. Inherits from `cache.root` when unset.", + value_type: "path", + default: "(inherited)", + }, + ConfigOptionDescription { + key: "cache.exec-environments", + description: "Cache directory for environments created by `pixi exec`. Inherits from `cache.root` when unset.", + value_type: "path", + default: "(inherited)", + }, + ConfigOptionDescription { + key: "cache.netfs-redirect", + description: "How node-local caches are redirected when the resolved cache root sits on a network filesystem.", + value_type: "one of `auto`, `always`, `never`", + default: "\"auto\"", + }, + ConfigOptionDescription { + key: "cache.pypi-mapping", + description: "Cache directory for the conda → PyPI name-mapping. Inherits from `cache.root` when unset.", + value_type: "path", + default: "(inherited)", + }, + ConfigOptionDescription { + key: "cache.pypi-wheels", + description: "Cache directory for the uv wheel cache. Inherits from `cache.root` when unset.", + value_type: "path", + default: "(inherited)", + }, + ConfigOptionDescription { + key: "cache.repodata", + description: "Cache directory for repodata. Inherits from `cache.root` when unset.", + value_type: "path", + default: "(inherited)", + }, + ConfigOptionDescription { + key: "cache.root", + description: "Root directory used to derive every per-kind cache subdirectory. Overrides the `PIXI_CACHE_DIR` / `RATTLER_CACHE_DIR` / XDG lookup.", + value_type: "path", + default: "(system default)", + }, + ConfigOptionDescription { + key: "concurrency", + description: "Concurrency limits. See `concurrency.*` keys; can also be set as a JSON object.", + value_type: "table", + default: "(defaults)", + }, + ConfigOptionDescription { + key: "concurrency.downloads", + description: "Maximum number of concurrent downloads.", + value_type: "integer", + default: "50", + }, + ConfigOptionDescription { + key: "concurrency.solves", + description: "Maximum number of concurrent solves.", + value_type: "integer", + default: "(available parallelism)", + }, + ConfigOptionDescription { + key: "default-channels", + description: "Channels selected when running `pixi init` or `pixi global install`. Once a workspace exists, its manifest channels are used instead.", + value_type: "array of channel names or URLs", + default: "[]", + }, + ConfigOptionDescription { + key: "detached-environments", + description: "Where to store environments. `true` keeps them in the cache directory, `false` keeps them in `.pixi/`, or set an explicit path.", + value_type: "bool or path", + default: "false", + }, + ConfigOptionDescription { + key: "experimental", + description: "Opt-in experimental features. See `experimental.*` keys; can also be set as a JSON object.", + value_type: "table", + default: "(empty)", + }, + ConfigOptionDescription { + key: "experimental.conda-script", + description: "Allow running `conda-script` files without passing `--experimental` on every invocation. The format follows a draft proposal and may still change.", + value_type: "bool", + default: "false", + }, + ConfigOptionDescription { + key: "experimental.use-environment-activation-cache", + description: "Cache activated environment variables between commands.", + value_type: "bool", + default: "false", + }, + ConfigOptionDescription { + key: "index-config", + description: "Options for the channels `pixi publish` indexes, with per-channel overrides keyed by channel URL or path. See `index-config.*` keys; can also be set as a JSON object.", + value_type: "table", + default: "(empty)", + }, + ConfigOptionDescription { + key: "index-config.base-url", + description: "`info.base_url` value written to generated repodata.", + value_type: "string", + default: "(unset)", + }, + ConfigOptionDescription { + key: "index-config.write-shards", + description: "Whether to write `repodata_shards.msgpack.zst` and shard files when indexing a channel.", + value_type: "bool", + default: "true", + }, + ConfigOptionDescription { + key: "index-config.write-zst", + description: "Whether to write `repodata.json.zst` when indexing a channel.", + value_type: "bool", + default: "true", + }, + ConfigOptionDescription { + key: "mirrors", + description: "Mirror redirects, mapping a channel URL to a list of mirror URLs.", + value_type: "map of URL → array of URLs", + default: "{}", + }, + ConfigOptionDescription { + key: "offline", + description: "Don't access the network and only use locally cached data. Operations that need data that is not cached will fail.", + value_type: "bool", + default: "false", + }, + ConfigOptionDescription { + key: "pinning-strategy", + description: "Version pinning strategy used by `pixi add`.", + value_type: "one of `semver`, `no-pin`, `exact-version`, `latest-up`, `major`, `minor`", + default: "\"semver\"", + }, + ConfigOptionDescription { + key: "proxy-config", + description: "HTTP/HTTPS proxy configuration. See `proxy-config.*` keys; can also be set as a JSON object.", + value_type: "table", + default: "(empty)", + }, + ConfigOptionDescription { + key: "proxy-config.http", + description: "Proxy URL used for HTTP requests.", + value_type: "URL", + default: "(unset)", + }, + ConfigOptionDescription { + key: "proxy-config.https", + description: "Proxy URL used for HTTPS requests.", + value_type: "URL", + default: "(unset)", + }, + ConfigOptionDescription { + key: "proxy-config.non-proxy-hosts", + description: "Hosts that should bypass the configured proxy.", + value_type: "array of strings", + default: "[]", + }, + ConfigOptionDescription { + key: "pypi-config", + description: "PyPI registry configuration. See `pypi-config.*` keys; can also be set as a JSON object.", + value_type: "table", + default: "(empty)", + }, + ConfigOptionDescription { + key: "pypi-config.allow-insecure-host", + description: "PyPI hosts for which TLS verification is skipped.", + value_type: "array of strings", + default: "[]", + }, + ConfigOptionDescription { + key: "pypi-config.extra-index-urls", + description: "Additional PyPI index URLs queried after the primary one.", + value_type: "array of URLs", + default: "[]", + }, + ConfigOptionDescription { + key: "pypi-config.index-url", + description: "Primary PyPI index URL.", + value_type: "URL", + default: "(unset)", + }, + ConfigOptionDescription { + key: "pypi-config.keyring-provider", + description: "Keyring provider used to fetch PyPI credentials.", + value_type: "one of `disabled`, `subprocess`", + default: "\"disabled\"", + }, + ConfigOptionDescription { + key: "repodata-config", + description: "Repodata fetch configuration. See `repodata-config.*` keys; can also be set as a JSON object.", + value_type: "table", + default: "(empty)", + }, + ConfigOptionDescription { + key: "repodata-config.disable-bzip2", + description: "Skip downloading the bzip2-compressed repodata variant.", + value_type: "bool", + default: "false", + }, + ConfigOptionDescription { + key: "repodata-config.disable-sharded", + description: "Skip downloading sharded repodata.", + value_type: "bool", + default: "false", + }, + ConfigOptionDescription { + key: "repodata-config.disable-zstd", + description: "Skip downloading the zstd-compressed repodata variant.", + value_type: "bool", + default: "false", + }, + ConfigOptionDescription { + key: "run-post-link-scripts", + description: "Whether package post-link scripts are executed. `insecure` runs them (they can execute arbitrary code).", + value_type: "one of `false`, `insecure`", + default: "\"false\"", + }, + ConfigOptionDescription { + key: "allow-symbolic-links", + description: "Allow symbolic links during package installation.", + value_type: "bool", + default: "(platform default)", + }, + ConfigOptionDescription { + key: "allow-hard-links", + description: "Allow hard links during package installation.", + value_type: "bool", + default: "(platform default)", + }, + ConfigOptionDescription { + key: "allow-ref-links", + description: "Allow ref links (copy-on-write) during package installation.", + value_type: "bool", + default: "(platform default)", + }, + ConfigOptionDescription { + key: "s3-options", + description: "Per-bucket S3 configuration. See `s3-options..*` keys; can also be set as a JSON object.", + value_type: "map of bucket name → table", + default: "{}", + }, + ConfigOptionDescription { + key: "s3-options.", + description: "Configuration for the S3 bucket ``. Can be set as a JSON object covering all three subkeys.", + value_type: "table", + default: "(empty)", + }, + ConfigOptionDescription { + key: "s3-options..addressing-style", + description: "How to address the S3 bucket ``: as a virtual host or through the path.", + value_type: "one of `virtual-host`, `path`", + default: "\"virtual-host\"", + }, + ConfigOptionDescription { + key: "s3-options..endpoint-url", + description: "Endpoint URL for the S3 bucket ``.", + value_type: "URL", + default: "(unset)", + }, + ConfigOptionDescription { + key: "s3-options..region", + description: "Region for the S3 bucket ``.", + value_type: "string", + default: "(unset)", + }, + ConfigOptionDescription { + key: "shell", + description: "Shell integration settings. See `shell.*` keys; can also be set as a JSON object.", + value_type: "table", + default: "(empty)", + }, + ConfigOptionDescription { + key: "shell.change-ps1", + description: "Add the `(pixi)` prefix to the shell prompt when an environment is active.", + value_type: "bool", + default: "true", + }, + ConfigOptionDescription { + key: "shell.force-activate", + description: "Force re-activation of the environment on every command, bypassing the activation cache.", + value_type: "bool", + default: "false", + }, + ConfigOptionDescription { + key: "shell.source-completion-scripts", + description: "Source the environment's autocompletion scripts when entering `pixi shell`.", + value_type: "bool", + default: "true", + }, + ConfigOptionDescription { + key: "tls-no-verify", + description: "Disable TLS certificate verification for all network connections. Use only for testing or trusted internal networks.", + value_type: "bool", + default: "false", + }, + ConfigOptionDescription { + key: "tls-root-certs", + description: "Which TLS root certificate store to use. `SSL_CERT_FILE` / `SSL_CERT_DIR` take precedence when set.", + value_type: "one of `webpki`, `system`", + default: "\"system\" (or \"webpki\" on rustls builds)", + }, + ConfigOptionDescription { + key: "tool-platform", + description: "Platform used when installing build backends and other tools. Useful when you want tooling for a different platform than the current one.", + value_type: "platform", + default: "(current platform)", + }, +]; + /// Controls which root certificates to use for TLS connections. /// /// Note: This setting only has an effect when pixi is built with the `rustls` feature. @@ -2019,6 +2361,11 @@ impl Config { ] } + /// Metadata for every configuration key, parallel to [`Self::get_keys`]. + pub fn describe_keys(&self) -> &'static [ConfigOptionDescription] { + CONFIG_OPTION_DESCRIPTIONS + } + /// Merge the `other` config into `self`. /// The `other` config will have higher priority #[must_use] @@ -4562,4 +4909,30 @@ UNUSED = "unused" let err = config.validate().unwrap_err(); assert!(format!("{err}").contains("cache.conda-packages")); } + + #[test] + fn describe_keys_matches_get_keys() { + let config = Config::default(); + let keys: Set<&str> = config.get_keys().iter().copied().collect(); + let described: Set<&str> = config.describe_keys().iter().map(|opt| opt.key).collect(); + let missing: Vec<&str> = keys.difference(&described).copied().collect(); + let extra: Vec<&str> = described.difference(&keys).copied().collect(); + assert!( + missing.is_empty() && extra.is_empty(), + "describe_keys() out of sync with get_keys(); missing: {missing:?}, extra: {extra:?}" + ); + } + + #[test] + fn describe_keys_have_non_empty_metadata() { + for opt in Config::default().describe_keys() { + assert!( + !opt.description.is_empty(), + "{} has no description", + opt.key + ); + assert!(!opt.value_type.is_empty(), "{} has no value_type", opt.key); + assert!(!opt.default.is_empty(), "{} has no default", opt.key); + } + } } diff --git a/docs/reference/cli/pixi/config/list.md b/docs/reference/cli/pixi/config/list.md index ae18dbdbec..8ee6e4c50d 100644 --- a/docs/reference/cli/pixi/config/list.md +++ b/docs/reference/cli/pixi/config/list.md @@ -20,6 +20,8 @@ pixi config list [OPTIONS] [KEY] ## Options - `--json` : Output in JSON format +- `--describe` +: Describe every configuration option with its type, default, and a short explanation ## Config Options - `--no-config` diff --git a/docs/reference/cli/pixi/config/list_extender b/docs/reference/cli/pixi/config/list_extender index 3739222d85..c15443a5d1 100644 --- a/docs/reference/cli/pixi/config/list_extender +++ b/docs/reference/cli/pixi/config/list_extender @@ -7,5 +7,7 @@ pixi config list default-channels pixi config list --json pixi config list --system pixi config list -g +pixi config list --describe +pixi config list --describe tls-no-verify ``` --8<-- [end:example] diff --git a/docs/reference/pixi_configuration.md b/docs/reference/pixi_configuration.md index abd0a29b2d..9cf737c9db 100644 --- a/docs/reference/pixi_configuration.md +++ b/docs/reference/pixi_configuration.md @@ -93,6 +93,19 @@ in `snake_case`: The following reference describes all available configuration options. +!!! tip "Describing options from the CLI" + Run `pixi config list --describe` to print every option with its type, default and a short explanation. + Options you have set are shown with their current value; the rest are commented out and show their default. + Pass a key to describe a single option, or add `--json` for machine-readable output. + + ```shell + $ pixi config list --describe tls-no-verify + # Disable TLS certificate verification for all network connections. Use only for testing or trusted internal networks. + # Type: bool + # Default: false + # tls-no-verify = false + ``` + ### `default-channels` The `default-channels` provide fallback channels for commands that do not obtain channels from an existing workspace or diff --git a/tests/integration_python/test_main_cli.py b/tests/integration_python/test_main_cli.py index 4e5d208f94..0c55de6133 100644 --- a/tests/integration_python/test_main_cli.py +++ b/tests/integration_python/test_main_cli.py @@ -907,6 +907,150 @@ def test_config_allow_links(pixi: Path, tmp_pixi_workspace: Path, dummy_channel_ ) +def test_config_describe(pixi: Path, tmp_pixi_workspace: Path, dummy_channel_1: str) -> None: + manifest_path = tmp_pixi_workspace / "pixi.toml" + verify_cli_command([pixi, "init", "--channel", dummy_channel_1, tmp_pixi_workspace]) + + # The `tmp_pixi_workspace` fixture seeds a project-local config that sets + # several of the keys asserted on below. Remove it, and pass `--no-config`, + # so the output only reflects built-in defaults and what this test sets. + (tmp_pixi_workspace / ".pixi" / "config.toml").unlink() + + verify_cli_command( + [pixi, "config", "list", "--describe", "--no-config", "--manifest-path", manifest_path], + stdout_contains=[ + "# Type: bool", + "# Default: false", + "# tls-no-verify = false", + "# default-channels = []", + "# concurrency.solves = (available parallelism)", + "# s3-options..region = (unset)", + ], + ) + + verify_cli_command( + [ + pixi, + "config", + "list", + "--describe", + "--no-config", + "tls-no-verify", + "--manifest-path", + manifest_path, + ], + stdout_contains=[ + "# Disable TLS certificate verification", + "# Type: bool", + "# tls-no-verify = false", + ], + stdout_excludes=["default-channels", "concurrency.solves"], + ) + + verify_cli_command( + [ + pixi, + "config", + "set", + "--manifest-path", + manifest_path, + "--local", + "tls-no-verify", + "true", + ] + ) + verify_cli_command( + [ + pixi, + "config", + "list", + "--describe", + "--no-config", + "tls-no-verify", + "--manifest-path", + manifest_path, + ], + stdout_contains=["tls-no-verify = true"], + stdout_excludes=["# tls-no-verify ="], + ) + + result = verify_cli_command( + [ + pixi, + "config", + "list", + "--describe", + "--no-config", + "--json", + "tls-no-verify", + "--manifest-path", + manifest_path, + ] + ) + parsed = json.loads(result.stdout) + assert len(parsed) == 1 + assert parsed[0]["key"] == "tls-no-verify" + assert parsed[0]["type"] == "bool" + assert parsed[0]["default"] == "false" + assert parsed[0]["value"] is True + + # Per-bucket keys can be described by a concrete bucket name or by the + # placeholder shown in the listing. + verify_cli_command( + [ + pixi, + "config", + "set", + "--manifest-path", + manifest_path, + "--local", + "s3-options", + '{"my-bucket": {"endpoint-url": "https://s3.example.com", "region": "eu-west-1", "addressing-style": "path"}}', + ] + ) + verify_cli_command( + [ + pixi, + "config", + "list", + "--describe", + "--no-config", + "s3-options.my-bucket.region", + "--manifest-path", + manifest_path, + ], + stdout_contains=["# Region for the S3 bucket", 's3-options.my-bucket.region = "eu-west-1"'], + ) + verify_cli_command( + [ + pixi, + "config", + "list", + "--describe", + "--no-config", + "s3-options..region", + "--manifest-path", + manifest_path, + ], + stdout_contains=["# s3-options..region = (unset)"], + ) + + verify_cli_command( + [ + pixi, + "config", + "list", + "--describe", + "--no-config", + "not-a-real-key", + "--manifest-path", + manifest_path, + ], + ExitCode.FAILURE, + stderr_contains="Unknown configuration key", + ) + + def test_dont_add_broken_dep(pixi: Path, tmp_pixi_workspace: Path, dummy_channel_1: str) -> None: manifest_path = tmp_pixi_workspace / "pixi.toml"