Deny some fields from the `pyproject.toml`

This commit is contained in:
Zanie Blue 2025-12-09 16:07:40 -06:00
parent 0daef692df
commit 47badd6f8b
6 changed files with 171 additions and 106 deletions

View File

@ -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,
));
}
}
_ => {}
}

View File

@ -195,6 +195,7 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<TokenStream> {
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<TokenStream> {
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<TokenStream> {
scope: #scope,
deprecated: #deprecated,
possible_values: #possible_values,
uv_toml_only: #uv_toml_only,
})
}
))
@ -266,6 +270,7 @@ struct FieldAttributes {
example: String,
scope: Option<String>,
possible_values: Option<bool>,
uv_toml_only: Option<bool>,
}
fn parse_field_attributes(attribute: &Attribute) -> syn::Result<FieldAttributes> {
@ -274,6 +279,7 @@ fn parse_field_attributes(attribute: &Attribute) -> syn::Result<FieldAttributes>
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<FieldAttributes>
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<FieldAttributes>
example,
scope,
possible_values,
uv_toml_only,
})
}

View File

@ -258,6 +258,8 @@ pub struct OptionField {
pub example: &'static str,
pub deprecated: Option<Deprecated>,
pub possible_values: Option<Vec<PossibleValue>>,
/// 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,
},
);

View File

@ -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,

View File

@ -214,7 +214,8 @@ pub struct GlobalOptions {
value_type = "bool",
example = r#"
native-tls = true
"#
"#,
uv_toml_only = true
)]
pub native_tls: Option<bool>,
/// 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<bool>,
/// 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<bool>,
/// 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<PathBuf>,
/// 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<String>,
/// 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<String>,
/// 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<Vec<String>>,
/// 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<Vec<TrustedHost>>,
}

View File

@ -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
```
---