diff --git a/Cargo.lock b/Cargo.lock index 4048247b1b61..5688c8210c85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5070,6 +5070,7 @@ dependencies = [ name = "uv-settings" version = "0.0.1" dependencies = [ + "clap", "dirs-sys", "distribution-types", "fs-err", diff --git a/crates/uv-dev/src/generate_options_reference.rs b/crates/uv-dev/src/generate_options_reference.rs index 4ffdf8b3b0be..cff030a92b41 100644 --- a/crates/uv-dev/src/generate_options_reference.rs +++ b/crates/uv-dev/src/generate_options_reference.rs @@ -216,7 +216,18 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S output.push_str("\n\n"); output.push_str(&format!("**Default value**: `{}`\n", field.default)); output.push('\n'); - output.push_str(&format!("**Type**: `{}`\n", field.value_type)); + if let Some(possible_values) = field + .possible_values + .as_ref() + .filter(|values| !values.is_empty()) + { + output.push_str("**Possible values**:\n\n"); + for value in possible_values { + output.push_str(format!("- {value}\n").as_str()); + } + } else { + output.push_str(&format!("**Type**: `{}`\n", field.value_type)); + } output.push('\n'); output.push_str("**Example usage**:\n\n"); output.push_str(&format_tab( diff --git a/crates/uv-macros/src/options_metadata.rs b/crates/uv-macros/src/options_metadata.rs index 255a2a4573e9..a060ac90004e 100644 --- a/crates/uv-macros/src/options_metadata.rs +++ b/crates/uv-macros/src/options_metadata.rs @@ -8,7 +8,7 @@ use syn::meta::ParseNestedMeta; use syn::spanned::Spanned; use syn::{ AngleBracketedGenericArguments, Attribute, Data, DataStruct, DeriveInput, ExprLit, Field, - Fields, Lit, LitStr, Meta, Path, PathArguments, PathSegment, Type, TypePath, + Fields, GenericArgument, Lit, LitStr, Meta, Path, PathArguments, PathSegment, Type, TypePath, }; use textwrap::dedent; @@ -194,6 +194,7 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result syn::Result::value_variants() + .iter() + .filter_map(clap::ValueEnum::to_possible_value) + .map(|value| uv_options_metadata::PossibleValue { + name: value.get_name().to_string(), + help: value.get_help().map(ToString::to_string), + }) + .collect() + ) + ) + } else { + quote!(None) + }; + Ok(quote_spanned!( ident.span() => { visit.record_field(#kebab_name, uv_options_metadata::OptionField{ @@ -232,7 +252,8 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result, + possible_values: Option, } fn parse_field_attributes(attribute: &Attribute) -> syn::Result { @@ -251,6 +273,7 @@ fn parse_field_attributes(attribute: &Attribute) -> syn::Result let mut value_type = None; let mut example = None; let mut scope = None; + let mut possible_values = None; attribute.parse_nested_meta(|meta| { if meta.path.is_ident("default") { @@ -262,6 +285,8 @@ fn parse_field_attributes(attribute: &Attribute) -> syn::Result } else if meta.path.is_ident("example") { let example_text = get_string_literal(&meta, "value_type", "option")?.value(); example = Some(dedent(&example_text).trim_matches('\n').to_string()); + } else if meta.path.is_ident("possible_values") { + possible_values = get_bool_literal(&meta, "possible_values", "option")?; } else { return Err(syn::Error::new( meta.path.span(), @@ -292,6 +317,7 @@ fn parse_field_attributes(attribute: &Attribute) -> syn::Result value_type, example, scope, + possible_values, }) } @@ -318,6 +344,23 @@ fn parse_deprecated_attribute(attribute: &Attribute) -> syn::Result Option<&Type> { + if let Type::Path(type_path) = ty { + if type_path.path.segments.len() == 1 && type_path.path.segments[0].ident == "Option" { + if let PathArguments::AngleBracketed(angle_bracketed_args) = + &type_path.path.segments[0].arguments + { + if angle_bracketed_args.args.len() == 1 { + if let GenericArgument::Type(inner_type) = &angle_bracketed_args.args[0] { + return Some(inner_type); + } + } + } + } + } + None +} + fn get_string_literal( meta: &ParseNestedMeta, meta_name: &str, @@ -351,6 +394,32 @@ fn get_string_literal( } } +fn get_bool_literal( + meta: &ParseNestedMeta, + meta_name: &str, + attribute_name: &str, +) -> syn::Result> { + let expr: syn::Expr = meta.value()?.parse()?; + + let mut value = &expr; + while let syn::Expr::Group(e) = value { + value = &e.expr; + } + + if let syn::Expr::Lit(ExprLit { + lit: Lit::Bool(lit), + .. + }) = value + { + Ok(Some(lit.value)) + } else { + Err(syn::Error::new( + expr.span(), + format!("expected {attribute_name} attribute to be a boolean: `{meta_name} = true`"), + )) + } +} + #[derive(Default, Debug)] struct DeprecatedAttribute { since: Option, diff --git a/crates/uv-options-metadata/src/lib.rs b/crates/uv-options-metadata/src/lib.rs index c5e9a34c97b8..8f7e9ec62b10 100644 --- a/crates/uv-options-metadata/src/lib.rs +++ b/crates/uv-options-metadata/src/lib.rs @@ -119,6 +119,7 @@ impl OptionSet { /// example: "", /// scope: None, /// deprecated: None, + /// possible_values: None /// }); /// } /// } @@ -141,7 +142,8 @@ impl OptionSet { /// value_type: "bool", /// example: "", /// scope: None, - /// deprecated: None + /// deprecated: None, + /// possible_values: None /// }); /// /// visit.record_set("format", Nested::metadata()); @@ -158,7 +160,8 @@ impl OptionSet { /// value_type: "bool", /// example: "", /// scope: None, - /// deprecated: None + /// deprecated: None, + /// possible_values: None /// }); /// } /// } @@ -190,7 +193,8 @@ impl OptionSet { /// value_type: "bool", /// example: "", /// scope: None, - /// deprecated: None + /// deprecated: None, + /// possible_values: None /// }; /// /// impl OptionsMetadata for WithOptions { @@ -213,7 +217,8 @@ impl OptionSet { /// value_type: "bool", /// example: "", /// scope: None, - /// deprecated: None + /// deprecated: None, + /// possible_values: None /// }; /// /// struct Root; @@ -226,7 +231,8 @@ impl OptionSet { /// value_type: "bool", /// example: "", /// scope: None, - /// deprecated: None + /// deprecated: None, + /// possible_values: None /// }); /// /// visit.record_set("format", Nested::metadata()); @@ -388,6 +394,7 @@ pub struct OptionField { pub scope: Option<&'static str>, pub example: &'static str, pub deprecated: Option, + pub possible_values: Option>, } #[derive(Debug, Clone, Eq, PartialEq, Serialize)] @@ -400,8 +407,22 @@ impl Display for OptionField { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { writeln!(f, "{}", self.doc)?; writeln!(f)?; + writeln!(f, "Default value: {}", self.default)?; - writeln!(f, "Type: {}", self.value_type)?; + + if let Some(possible_values) = self + .possible_values + .as_ref() + .filter(|values| !values.is_empty()) + { + writeln!(f, "Possible values:")?; + writeln!(f)?; + for value in possible_values { + writeln!(f, "- {value}")?; + } + } else { + writeln!(f, "Type: {}", self.value_type)?; + } if let Some(deprecated) = &self.deprecated { write!(f, "Deprecated")?; @@ -420,3 +441,21 @@ impl Display for OptionField { writeln!(f, "Example usage:\n```toml\n{}\n```", self.example) } } + +/// A possible value for an enum, similar to Clap's `PossibleValue` type (but without a dependency +/// on Clap). +#[derive(Debug, Eq, PartialEq, Clone, Serialize)] +pub struct PossibleValue { + pub name: String, + pub help: Option, +} + +impl Display for PossibleValue { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "`\"{}\"`", self.name)?; + if let Some(help) = &self.help { + write!(f, ": {help}")?; + } + Ok(()) + } +} diff --git a/crates/uv-settings/Cargo.toml b/crates/uv-settings/Cargo.toml index 2d99d63355ce..b81f8aae2c50 100644 --- a/crates/uv-settings/Cargo.toml +++ b/crates/uv-settings/Cargo.toml @@ -14,18 +14,19 @@ workspace = true [dependencies] distribution-types = { workspace = true, features = ["schemars"] } -install-wheel-rs = { workspace = true, features = ["schemars"] } +install-wheel-rs = { workspace = true, features = ["schemars", "clap"] } pep508_rs = { workspace = true } pypi-types = { workspace = true } -uv-configuration = { workspace = true, features = ["schemars"] } +uv-configuration = { workspace = true, features = ["schemars", "clap"] } uv-fs = { workspace = true } uv-macros = { workspace = true } uv-normalize = { workspace = true, features = ["schemars"] } uv-options-metadata = { workspace = true } -uv-python = { workspace = true, features = ["schemars"] } -uv-resolver = { workspace = true, features = ["schemars"] } +uv-python = { workspace = true, features = ["schemars", "clap"] } +uv-resolver = { workspace = true, features = ["schemars", "clap"] } uv-warnings = { workspace = true } +clap = { workspace = true } dirs-sys = { workspace = true } fs-err = { workspace = true } schemars = { workspace = true, optional = true } @@ -35,4 +36,4 @@ toml = { workspace = true } tracing = { workspace = true } [package.metadata.cargo-shear] -ignored = ["uv-options-metadata"] +ignored = ["uv-options-metadata", "clap"] diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index fb2087d73c0f..3e148a5318ac 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -120,7 +120,8 @@ pub struct GlobalOptions { value_type = "str", example = r#" python-preference = "managed" - "# + "#, + possible_values = true )] pub python_preference: Option, /// Whether to automatically download Python when required. @@ -129,7 +130,8 @@ pub struct GlobalOptions { value_type = "str", example = r#" python-fetch = \"automatic\" - "# + "#, + possible_values = true )] pub python_fetch: Option, } @@ -255,18 +257,13 @@ pub struct ResolverInstallerOptions { /// limit resolutions to those present on that first index (`first-match`). This prevents /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the /// same name to a secondary. - /// - /// Possible values: - /// - /// - `"first-index"`: Only use results from the first index that returns a match for a given package name. - /// - `"unsafe-first-match"`: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next. - /// - `"unsafe-best-match"`: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index. #[option( default = "\"first-index\"", value_type = "str", example = r#" index-strategy = "unsafe-best-match" - "# + "#, + possible_values = true )] pub index_strategy: Option, /// Attempt to use `keyring` for authentication for index URLs. @@ -290,7 +287,8 @@ pub struct ResolverInstallerOptions { value_type = "str", example = r#" resolution = "lowest-direct" - "# + "#, + possible_values = true )] pub resolution: Option, /// The strategy to use when considering pre-release versions. @@ -303,7 +301,8 @@ pub struct ResolverInstallerOptions { value_type = "str", example = r#" prerelease = "allow" - "# + "#, + possible_values = true )] pub prerelease: Option, /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. @@ -336,7 +335,8 @@ pub struct ResolverInstallerOptions { value_type = "str", example = r#" link-mode = "copy" - "# + "#, + possible_values = true )] pub link_mode: Option, /// Compile Python files to bytecode after installation. @@ -590,18 +590,13 @@ pub struct PipOptions { /// limit resolutions to those present on that first index (`first-match`). This prevents /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the /// same name to a secondary. - /// - /// Possible values: - /// - /// - `"first-index"`: Only use results from the first index that returns a match for a given package name. - /// - `"unsafe-first-match"`: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next. - /// - `"unsafe-best-match"`: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index. #[option( default = "\"first-index\"", value_type = "str", example = r#" index-strategy = "unsafe-best-match" - "# + "#, + possible_values = true )] pub index_strategy: Option, /// Attempt to use `keyring` for authentication for index URLs. @@ -734,7 +729,8 @@ pub struct PipOptions { value_type = "str", example = r#" resolution = "lowest-direct" - "# + "#, + possible_values = true )] pub resolution: Option, /// The strategy to use when considering pre-release versions. @@ -747,7 +743,8 @@ pub struct PipOptions { value_type = "str", example = r#" prerelease = "allow" - "# + "#, + possible_values = true )] pub prerelease: Option, /// Write the requirements generated by `uv pip compile` to the given `requirements.txt` file. @@ -966,7 +963,8 @@ pub struct PipOptions { value_type = "str", example = r#" annotation-style = "line" - "# + "#, + possible_values = true )] pub annotation_style: Option, /// The method to use when installing packages from the global cache. @@ -978,7 +976,8 @@ pub struct PipOptions { value_type = "str", example = r#" link-mode = "copy" - "# + "#, + possible_values = true )] pub link_mode: Option, /// Compile Python files to bytecode after installation. diff --git a/docs/settings.md b/docs/settings.md index 2b3228ac50b1..14226474d949 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -189,15 +189,13 @@ limit resolutions to those present on that first index (`first-match`). This pre "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary. -Possible values: - -- `"first-index"`: Only use results from the first index that returns a match for a given package name. -- `"unsafe-first-match"`: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next. -- `"unsafe-best-match"`: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index. - **Default value**: `"first-index"` -**Type**: `str` +**Possible values**: + +- `"first-index"`: Only use results from the first index that returns a match for a given package name +- `"unsafe-first-match"`: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next +- `"unsafe-best-match"`: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index **Example usage**: @@ -284,7 +282,11 @@ Windows. **Default value**: `"clone" (macOS) or "hardlink" (Linux, Windows)` -**Type**: `str` +**Possible values**: + +- `"clone"`: Clone (i.e., copy-on-write) packages from the wheel into the site packages +- `"copy"`: Copy packages from the wheel into the site packages +- `"hardlink"`: Hard link packages from the wheel into the site packages **Example usage**: @@ -556,7 +558,13 @@ declared specifiers (`if-necessary-or-explicit`). **Default value**: `"if-necessary-or-explicit"` -**Type**: `str` +**Possible values**: + +- `"disallow"`: Disallow all pre-release versions +- `"allow"`: Allow all pre-release versions +- `"if-necessary"`: Allow pre-release versions if all versions of a package are pre-release +- `"explicit"`: Allow pre-release versions for first-party packages with explicit pre-release markers in their version requirements +- `"if-necessary-or-explicit"`: Allow pre-release versions if all versions of a package are pre-release, or if the package has an explicit pre-release marker in its version requirements **Example usage**: @@ -606,7 +614,10 @@ Whether to automatically download Python when required. **Default value**: `"automatic"` -**Type**: `str` +**Possible values**: + +- `"automatic"`: Automatically fetch managed Python installations when needed +- `"manual"`: Do not automatically fetch managed Python installations; require explicit installation **Example usage**: @@ -632,7 +643,13 @@ those that are downloaded and installed by uv. **Default value**: `"installed"` -**Type**: `str` +**Possible values**: + +- `"only-managed"`: Only use managed Python installations; never use system Python installations +- `"installed"`: Prefer installed Python installations, only download managed Python installations if no system Python installation is found +- `"managed"`: Prefer managed Python installations over system Python installations, even if fetching is required +- `"system"`: Prefer system Python installations over managed Python installations +- `"only-system"`: Only use system Python installations; never use managed Python installations **Example usage**: @@ -710,7 +727,11 @@ By default, uv will use the latest compatible version of each package (`highest` **Default value**: `"highest"` -**Type**: `str` +**Possible values**: + +- `"highest"`: Resolve the highest compatible version of each package +- `"lowest"`: Resolve the lowest compatible version of each package +- `"lowest-direct"`: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies **Example usage**: @@ -852,7 +873,10 @@ source of each package. **Default value**: `"split"` -**Type**: `str` +**Possible values**: + +- `"line"`: Render the annotations on a single, comma-separated line +- `"split"`: Render each annotation on its own line **Example usage**: @@ -1372,15 +1396,13 @@ limit resolutions to those present on that first index (`first-match`). This pre "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary. -Possible values: - -- `"first-index"`: Only use results from the first index that returns a match for a given package name. -- `"unsafe-first-match"`: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next. -- `"unsafe-best-match"`: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index. - **Default value**: `"first-index"` -**Type**: `str` +**Possible values**: + +- `"first-index"`: Only use results from the first index that returns a match for a given package name +- `"unsafe-first-match"`: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next +- `"unsafe-best-match"`: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index **Example usage**: @@ -1497,7 +1519,11 @@ Windows. **Default value**: `"clone" (macOS) or "hardlink" (Linux, Windows)` -**Type**: `str` +**Possible values**: + +- `"clone"`: Clone (i.e., copy-on-write) packages from the wheel into the site packages +- `"copy"`: Copy packages from the wheel into the site packages +- `"hardlink"`: Hard link packages from the wheel into the site packages **Example usage**: @@ -1906,7 +1932,13 @@ declared specifiers (`if-necessary-or-explicit`). **Default value**: `"if-necessary-or-explicit"` -**Type**: `str` +**Possible values**: + +- `"disallow"`: Disallow all pre-release versions +- `"allow"`: Allow all pre-release versions +- `"if-necessary"`: Allow pre-release versions if all versions of a package are pre-release +- `"explicit"`: Allow pre-release versions for first-party packages with explicit pre-release markers in their version requirements +- `"if-necessary-or-explicit"`: Allow pre-release versions if all versions of a package are pre-release, or if the package has an explicit pre-release marker in its version requirements **Example usage**: @@ -2121,7 +2153,11 @@ By default, uv will use the latest compatible version of each package (`highest` **Default value**: `"highest"` -**Type**: `str` +**Possible values**: + +- `"highest"`: Resolve the highest compatible version of each package +- `"lowest"`: Resolve the lowest compatible version of each package +- `"lowest-direct"`: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies **Example usage**: diff --git a/uv.schema.json b/uv.schema.json index fbfa8e715be4..e207a6db8c70 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -71,7 +71,7 @@ } }, "index-strategy": { - "description": "The strategy to use when resolving against multiple index URLs.\n\nBy default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (`first-match`). This prevents \"dependency confusion\" attacks, whereby an attack can upload a malicious package under the same name to a secondary.\n\nPossible values:\n\n- `\"first-index\"`: Only use results from the first index that returns a match for a given package name. - `\"unsafe-first-match\"`: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next. - `\"unsafe-best-match\"`: Search for every package name across all indexes, preferring the \"best\" version found. If a package version is in multiple indexes, only look at the entry for the first index.", + "description": "The strategy to use when resolving against multiple index URLs.\n\nBy default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (`first-match`). This prevents \"dependency confusion\" attacks, whereby an attack can upload a malicious package under the same name to a secondary.", "anyOf": [ { "$ref": "#/definitions/IndexStrategy" @@ -638,7 +638,7 @@ ] }, "index-strategy": { - "description": "The strategy to use when resolving against multiple index URLs.\n\nBy default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (`first-match`). This prevents \"dependency confusion\" attacks, whereby an attack can upload a malicious package under the same name to a secondary.\n\nPossible values:\n\n- `\"first-index\"`: Only use results from the first index that returns a match for a given package name. - `\"unsafe-first-match\"`: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next. - `\"unsafe-best-match\"`: Search for every package name across all indexes, preferring the \"best\" version found. If a package version is in multiple indexes, only look at the entry for the first index.", + "description": "The strategy to use when resolving against multiple index URLs.\n\nBy default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (`first-match`). This prevents \"dependency confusion\" attacks, whereby an attack can upload a malicious package under the same name to a secondary.", "anyOf": [ { "$ref": "#/definitions/IndexStrategy"