From 47badd6f8bad1bb84c31c14d5ac666a825a98ebb Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 9 Dec 2025 16:07:40 -0600 Subject: [PATCH] Deny some fields from the `pyproject.toml` --- .../uv-dev/src/generate_options_reference.rs | 50 ++++++--- crates/uv-macros/src/options_metadata.rs | 9 ++ crates/uv-options-metadata/src/lib.rs | 8 ++ crates/uv-settings/src/lib.rs | 82 ++++++++++++++ crates/uv-settings/src/settings.rs | 24 ++-- docs/reference/settings.md | 104 ++++-------------- 6 files changed, 171 insertions(+), 106 deletions(-) diff --git a/crates/uv-dev/src/generate_options_reference.rs b/crates/uv-dev/src/generate_options_reference.rs index 83ee2d003..579a02f08 100644 --- a/crates/uv-dev/src/generate_options_reference.rs +++ b/crates/uv-dev/src/generate_options_reference.rs @@ -283,26 +283,40 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S option_type: OptionType::Configuration, .. } => { - output.push_str(&format_tab( - "pyproject.toml", - &format_header( - field.scope, + if field.uv_toml_only { + // Only show uv.toml example for fields that are not allowed in pyproject.toml + output.push_str(&format_code( + "uv.toml", + &format_header( + field.scope, + field.example, + parents, + ConfigurationFile::UvToml, + ), field.example, - parents, - ConfigurationFile::PyprojectToml, - ), - field.example, - )); - output.push_str(&format_tab( - "uv.toml", - &format_header( - field.scope, + )); + } else { + output.push_str(&format_tab( + "pyproject.toml", + &format_header( + field.scope, + field.example, + parents, + ConfigurationFile::PyprojectToml, + ), field.example, - parents, - ConfigurationFile::UvToml, - ), - field.example, - )); + )); + output.push_str(&format_tab( + "uv.toml", + &format_header( + field.scope, + field.example, + parents, + ConfigurationFile::UvToml, + ), + field.example, + )); + } } _ => {} } diff --git a/crates/uv-macros/src/options_metadata.rs b/crates/uv-macros/src/options_metadata.rs index 127ccae14..a0b6ed95d 100644 --- a/crates/uv-macros/src/options_metadata.rs +++ b/crates/uv-macros/src/options_metadata.rs @@ -195,6 +195,7 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result { example, scope, possible_values, + uv_toml_only, } = parse_field_attributes(attr)?; let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span()); @@ -244,6 +245,8 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result { quote!(None) }; + let uv_toml_only = uv_toml_only.unwrap_or(false); + Ok(quote_spanned!( ident.span() => { visit.record_field(#kebab_name, uv_options_metadata::OptionField{ @@ -254,6 +257,7 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result { scope: #scope, deprecated: #deprecated, possible_values: #possible_values, + uv_toml_only: #uv_toml_only, }) } )) @@ -266,6 +270,7 @@ struct FieldAttributes { example: String, scope: Option, possible_values: Option, + uv_toml_only: Option, } fn parse_field_attributes(attribute: &Attribute) -> syn::Result { @@ -274,6 +279,7 @@ fn parse_field_attributes(attribute: &Attribute) -> syn::Result let mut example = None; let mut scope = None; let mut possible_values = None; + let mut uv_toml_only = None; attribute.parse_nested_meta(|meta| { if meta.path.is_ident("default") { @@ -287,6 +293,8 @@ fn parse_field_attributes(attribute: &Attribute) -> syn::Result 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 if meta.path.is_ident("uv_toml_only") { + uv_toml_only = get_bool_literal(&meta, "uv_toml_only", "option")?; } else { return Err(syn::Error::new( meta.path.span(), @@ -327,6 +335,7 @@ fn parse_field_attributes(attribute: &Attribute) -> syn::Result example, scope, possible_values, + uv_toml_only, }) } diff --git a/crates/uv-options-metadata/src/lib.rs b/crates/uv-options-metadata/src/lib.rs index 8b8d38aa2..9448381b0 100644 --- a/crates/uv-options-metadata/src/lib.rs +++ b/crates/uv-options-metadata/src/lib.rs @@ -258,6 +258,8 @@ pub struct OptionField { pub example: &'static str, pub deprecated: Option, pub possible_values: Option>, + /// If `true`, the option is only available in `uv.toml`, not `pyproject.toml`. + pub uv_toml_only: bool, } #[derive(Debug, Clone, Eq, PartialEq, Serialize)] @@ -343,6 +345,7 @@ mod tests { scope: None, deprecated: None, possible_values: None, + uv_toml_only: false, }, ); } @@ -368,6 +371,7 @@ mod tests { scope: None, deprecated: None, possible_values: None, + uv_toml_only: false, }, ); @@ -389,6 +393,7 @@ mod tests { scope: None, deprecated: None, possible_values: None, + uv_toml_only: false, }, ); } @@ -411,6 +416,7 @@ mod tests { scope: None, deprecated: None, possible_values: None, + uv_toml_only: false, }; impl OptionsMetadata for WithOptions { @@ -436,6 +442,7 @@ mod tests { scope: None, deprecated: None, possible_values: None, + uv_toml_only: false, }; struct Root; @@ -452,6 +459,7 @@ mod tests { scope: None, deprecated: None, possible_values: None, + uv_toml_only: false, }, ); diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index 862cadc08..868232431 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -162,6 +162,7 @@ impl FilesystemOptions { let options = options.relative_to(&std::path::absolute(dir)?)?; tracing::debug!("Found workspace configuration at `{}`", path.display()); + validate_pyproject_toml(&path, &options)?; return Ok(Some(Self(options))); } Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} @@ -284,6 +285,83 @@ fn validate_uv_toml(path: &Path, options: &Options) -> Result<(), Error> { Ok(()) } +/// Validate that an [`Options`] schema is compatible with `pyproject.toml`. +/// +/// Some settings are user-specific and should not be in a `pyproject.toml` file +/// that may be checked into version control. +fn validate_pyproject_toml(path: &Path, options: &Options) -> Result<(), Error> { + let Options { + globals: + GlobalOptions { + required_version: _, + native_tls, + offline, + no_cache, + cache_dir, + preview: _, + python_preference: _, + python_downloads: _, + concurrent_downloads: _, + concurrent_builds: _, + concurrent_installs: _, + http_proxy, + https_proxy, + no_proxy, + allow_insecure_host, + }, + top_level: _, + install_mirrors: _, + publish: _, + add: _, + pip: _, + cache_keys: _, + override_dependencies: _, + exclude_dependencies: _, + constraint_dependencies: _, + build_constraint_dependencies: _, + environments: _, + required_environments: _, + conflicts: _, + workspace: _, + sources: _, + dev_dependencies: _, + default_groups: _, + dependency_groups: _, + managed: _, + package: _, + build_backend: _, + } = options; + + if native_tls.is_some() { + return Err(Error::UvTomlOnlyField(path.to_path_buf(), "native-tls")); + } + if offline.is_some() { + return Err(Error::UvTomlOnlyField(path.to_path_buf(), "offline")); + } + if no_cache.is_some() { + return Err(Error::UvTomlOnlyField(path.to_path_buf(), "no-cache")); + } + if cache_dir.is_some() { + return Err(Error::UvTomlOnlyField(path.to_path_buf(), "cache-dir")); + } + if allow_insecure_host.is_some() { + return Err(Error::UvTomlOnlyField( + path.to_path_buf(), + "allow-insecure-host", + )); + } + if http_proxy.is_some() { + return Err(Error::UvTomlOnlyField(path.to_path_buf(), "http-proxy")); + } + if https_proxy.is_some() { + return Err(Error::UvTomlOnlyField(path.to_path_buf(), "https-proxy")); + } + if no_proxy.is_some() { + return Err(Error::UvTomlOnlyField(path.to_path_buf(), "no-proxy")); + } + Ok(()) +} + /// Validate that an [`Options`] contains no fields that `uv.toml` would mask /// /// This is essentially the inverse of [`validated_uv_toml`][]. @@ -575,6 +653,10 @@ pub enum Error { )] PyprojectOnlyField(PathBuf, &'static str), + #[error("Failed to parse: `{}`. The `{}` field is not allowed in a `pyproject.toml` file. `{}` is user-specific and should be placed in a `uv.toml` file, set via environment variable, or passed via the CLI instead.", _0.user_display(), _1, _1 + )] + UvTomlOnlyField(PathBuf, &'static str), + #[error("Failed to parse environment variable `{name}` with invalid value `{value}`: {err}")] InvalidEnvironmentVariable { name: String, diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 6c1da5c31..8d1227804 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -214,7 +214,8 @@ pub struct GlobalOptions { value_type = "bool", example = r#" native-tls = true - "# + "#, + uv_toml_only = true )] pub native_tls: Option, /// Disable network access, relying only on locally cached data and locally available files. @@ -223,7 +224,8 @@ pub struct GlobalOptions { value_type = "bool", example = r#" offline = true - "# + "#, + uv_toml_only = true )] pub offline: Option, /// Avoid reading from or writing to the cache, instead using a temporary directory for the @@ -233,7 +235,8 @@ pub struct GlobalOptions { value_type = "bool", example = r#" no-cache = true - "# + "#, + uv_toml_only = true )] pub no_cache: Option, /// Path to the cache directory. @@ -245,7 +248,8 @@ pub struct GlobalOptions { value_type = "str", example = r#" cache-dir = "./.uv_cache" - "# + "#, + uv_toml_only = true )] pub cache_dir: Option, /// Whether to enable experimental, preview features. @@ -317,7 +321,8 @@ pub struct GlobalOptions { value_type = "str", example = r#" http-proxy = "http://proxy.example.com" - "# + "#, + uv_toml_only = true )] pub http_proxy: Option, /// The URL of the HTTPS proxy to use. @@ -326,7 +331,8 @@ pub struct GlobalOptions { value_type = "str", example = r#" https-proxy = "https://proxy.example.com" - "# + "#, + uv_toml_only = true )] pub https_proxy: Option, /// A list of hosts to exclude from proxying. @@ -335,7 +341,8 @@ pub struct GlobalOptions { value_type = "list[str]", example = r#" no-proxy = ["localhost", "127.0.0.1"] - "# + "#, + uv_toml_only = true )] pub no_proxy: Option>, /// Allow insecure connections to host. @@ -351,7 +358,8 @@ pub struct GlobalOptions { value_type = "list[str]", example = r#" allow-insecure-host = ["localhost:8080"] - "# + "#, + uv_toml_only = true )] pub allow_insecure_host: Option>, } diff --git a/docs/reference/settings.md b/docs/reference/settings.md index ca9b05e80..067fe96e0 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -766,17 +766,10 @@ bypasses SSL verification and could expose you to MITM attacks. **Example usage**: -=== "pyproject.toml" +```toml title="uv.toml" - ```toml - [tool.uv] - allow-insecure-host = ["localhost:8080"] - ``` -=== "uv.toml" - - ```toml - allow-insecure-host = ["localhost:8080"] - ``` +allow-insecure-host = ["localhost:8080"] +``` --- @@ -793,17 +786,10 @@ Defaults to `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` on Linux and macOS, and **Example usage**: -=== "pyproject.toml" +```toml title="uv.toml" - ```toml - [tool.uv] - cache-dir = "./.uv_cache" - ``` -=== "uv.toml" - - ```toml - cache-dir = "./.uv_cache" - ``` +cache-dir = "./.uv_cache" +``` --- @@ -1326,17 +1312,10 @@ The URL of the HTTP proxy to use. **Example usage**: -=== "pyproject.toml" +```toml title="uv.toml" - ```toml - [tool.uv] - http-proxy = "http://proxy.example.com" - ``` -=== "uv.toml" - - ```toml - http-proxy = "http://proxy.example.com" - ``` +http-proxy = "http://proxy.example.com" +``` --- @@ -1350,17 +1329,10 @@ The URL of the HTTPS proxy to use. **Example usage**: -=== "pyproject.toml" +```toml title="uv.toml" - ```toml - [tool.uv] - https-proxy = "https://proxy.example.com" - ``` -=== "uv.toml" - - ```toml - https-proxy = "https://proxy.example.com" - ``` +https-proxy = "https://proxy.example.com" +``` --- @@ -1564,17 +1536,10 @@ included in your system's certificate store. **Example usage**: -=== "pyproject.toml" +```toml title="uv.toml" - ```toml - [tool.uv] - native-tls = true - ``` -=== "uv.toml" - - ```toml - native-tls = true - ``` +native-tls = true +``` --- @@ -1746,17 +1711,10 @@ duration of the operation. **Example usage**: -=== "pyproject.toml" +```toml title="uv.toml" - ```toml - [tool.uv] - no-cache = true - ``` -=== "uv.toml" - - ```toml - no-cache = true - ``` +no-cache = true +``` --- @@ -1795,17 +1753,10 @@ A list of hosts to exclude from proxying. **Example usage**: -=== "pyproject.toml" +```toml title="uv.toml" - ```toml - [tool.uv] - no-proxy = ["localhost", "127.0.0.1"] - ``` -=== "uv.toml" - - ```toml - no-proxy = ["localhost", "127.0.0.1"] - ``` +no-proxy = ["localhost", "127.0.0.1"] +``` --- @@ -1845,17 +1796,10 @@ Disable network access, relying only on locally cached data and locally availabl **Example usage**: -=== "pyproject.toml" +```toml title="uv.toml" - ```toml - [tool.uv] - offline = true - ``` -=== "uv.toml" - - ```toml - offline = true - ``` +offline = true +``` ---