Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error when disallowed settings are defined in uv.toml #8550

Merged
merged 2 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/uv-settings/src/combine.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::num::NonZeroUsize;
use std::path::PathBuf;

use url::Url;

use uv_configuration::{
Expand Down Expand Up @@ -124,3 +125,9 @@ impl Combine for serde::de::IgnoredAny {
self
}
}

impl Combine for Option<serde::de::IgnoredAny> {
fn combine(self, _other: Self) -> Self {
self
}
}
52 changes: 42 additions & 10 deletions crates/uv-settings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ impl FilesystemOptions {
match read_file(&file) {
Ok(options) => {
debug!("Found user configuration in: `{}`", file.display());
validate_uv_toml(&file, &options)?;
Ok(Some(Self(options)))
}
Err(Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Expand Down Expand Up @@ -83,11 +84,11 @@ impl FilesystemOptions {
Ok(None) => {
// Continue traversing the directory tree.
}
Err(Error::PyprojectToml(file, err)) => {
Err(Error::PyprojectToml(path, err)) => {
// If we see an invalid `pyproject.toml`, warn but continue.
warn_user!(
"Failed to parse `{}` during settings discovery:\n{}",
file.cyan(),
path.user_display().cyan(),
textwrap::indent(&err.to_string(), " ")
);
}
Expand All @@ -108,7 +109,7 @@ impl FilesystemOptions {
match fs_err::read_to_string(&path) {
Ok(content) => {
let options: Options = toml::from_str(&content)
.map_err(|err| Error::UvToml(path.user_display().to_string(), err))?;
.map_err(|err| Error::UvToml(path.clone(), Box::new(err)))?;

// If the directory also contains a `[tool.uv]` table in a `pyproject.toml` file,
// warn.
Expand All @@ -125,6 +126,8 @@ impl FilesystemOptions {
}

debug!("Found workspace configuration at `{}`", path.display());

validate_uv_toml(&path, &options)?;
return Ok(Some(Self(options)));
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Expand All @@ -137,7 +140,7 @@ impl FilesystemOptions {
Ok(content) => {
// Parse, but skip any `pyproject.toml` that doesn't have a `[tool.uv]` section.
let pyproject: PyProjectToml = toml::from_str(&content)
.map_err(|err| Error::PyprojectToml(path.user_display().to_string(), err))?;
.map_err(|err| Error::PyprojectToml(path.clone(), Box::new(err)))?;
let Some(tool) = pyproject.tool else {
debug!(
"Skipping `pyproject.toml` in `{}` (no `[tool]` section)",
Expand Down Expand Up @@ -238,21 +241,50 @@ fn system_config_file() -> Option<PathBuf> {
/// Load [`Options`] from a `uv.toml` file.
fn read_file(path: &Path) -> Result<Options, Error> {
let content = fs_err::read_to_string(path)?;
let options: Options = toml::from_str(&content)
.map_err(|err| Error::UvToml(path.user_display().to_string(), err))?;
let options: Options =
toml::from_str(&content).map_err(|err| Error::UvToml(path.to_path_buf(), Box::new(err)))?;
Ok(options)
}

/// Validate that an [`Options`] schema is compatible with `uv.toml`.
fn validate_uv_toml(path: &Path, options: &Options) -> Result<(), Error> {
// The `uv.toml` format is not allowed to include any of the following, which are
// permitted by the schema since they _can_ be included in `pyproject.toml` files
// (and we want to use `deny_unknown_fields`).
if options.workspace.is_some() {
return Err(Error::PyprojectOnlyField(path.to_path_buf(), "workspace"));
}
if options.sources.is_some() {
return Err(Error::PyprojectOnlyField(path.to_path_buf(), "sources"));
}
if options.dev_dependencies.is_some() {
return Err(Error::PyprojectOnlyField(
path.to_path_buf(),
"dev-dependencies",
));
}
if options.managed.is_some() {
return Err(Error::PyprojectOnlyField(path.to_path_buf(), "managed"));
}
if options.package.is_some() {
return Err(Error::PyprojectOnlyField(path.to_path_buf(), "package"));
}
Ok(())
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),

#[error("Failed to parse: `{0}`")]
PyprojectToml(String, #[source] toml::de::Error),
#[error("Failed to parse: `{}`", _0.user_display())]
PyprojectToml(PathBuf, #[source] Box<toml::de::Error>),

#[error("Failed to parse: `{}`", _0.user_display())]
UvToml(PathBuf, #[source] Box<toml::de::Error>),

#[error("Failed to parse: `{0}`")]
UvToml(String, #[source] toml::de::Error),
#[error("Failed to parse: `{}`. The `{1}` field is not allowed in a `uv.toml` file. `{1}` is only applicable in the context of a project, and should be placed in a `pyproject.toml` file instead.", _0.user_display())]
PyprojectOnlyField(PathBuf, &'static str),
}

#[cfg(test)]
Expand Down
55 changes: 38 additions & 17 deletions crates/uv-settings/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ pub struct Options {
cache_keys: Option<Vec<CacheKey>>,

// NOTE(charlie): These fields are shared with `ToolUv` in
// `crates/uv-workspace/src/pyproject.rs`, and the documentation lives on that struct.
// `crates/uv-workspace/src/pyproject.rs`. The documentation lives on that struct.
// They're respected in both `pyproject.toml` and `uv.toml` files.
#[cfg_attr(feature = "schemars", schemars(skip))]
pub override_dependencies: Option<Vec<Requirement<VerbatimParsedUrl>>>,

Expand All @@ -95,6 +96,24 @@ pub struct Options {

#[cfg_attr(feature = "schemars", schemars(skip))]
pub environments: Option<SupportedEnvironments>,

// NOTE(charlie): These fields should be kept in-sync with `ToolUv` in
// `crates/uv-workspace/src/pyproject.rs`. The documentation lives on that struct.
// They're only respected in `pyproject.toml` files, and should be rejected in `uv.toml` files.
#[cfg_attr(feature = "schemars", schemars(skip))]
pub workspace: Option<serde::de::IgnoredAny>,

#[cfg_attr(feature = "schemars", schemars(skip))]
pub sources: Option<serde::de::IgnoredAny>,

#[cfg_attr(feature = "schemars", schemars(skip))]
pub dev_dependencies: Option<serde::de::IgnoredAny>,

#[cfg_attr(feature = "schemars", schemars(skip))]
pub managed: Option<serde::de::IgnoredAny>,

#[cfg_attr(feature = "schemars", schemars(skip))]
pub r#package: Option<serde::de::IgnoredAny>,
}

impl Options {
Expand Down Expand Up @@ -1551,22 +1570,19 @@ pub struct OptionsWire {
cache_keys: Option<Vec<CacheKey>>,

// NOTE(charlie): These fields are shared with `ToolUv` in
// `crates/uv-workspace/src/pyproject.rs`, and the documentation lives on that struct.
// `crates/uv-workspace/src/pyproject.rs`. The documentation lives on that struct.
// They're respected in both `pyproject.toml` and `uv.toml` files.
override_dependencies: Option<Vec<Requirement<VerbatimParsedUrl>>>,
constraint_dependencies: Option<Vec<Requirement<VerbatimParsedUrl>>>,
environments: Option<SupportedEnvironments>,

// NOTE(charlie): These fields should be kept in-sync with `ToolUv` in
// `crates/uv-workspace/src/pyproject.rs`.
#[allow(dead_code)]
// `crates/uv-workspace/src/pyproject.rs`. The documentation lives on that struct.
// They're only respected in `pyproject.toml` files, and should be rejected in `uv.toml` files.
workspace: Option<serde::de::IgnoredAny>,
#[allow(dead_code)]
sources: Option<serde::de::IgnoredAny>,
#[allow(dead_code)]
dev_dependencies: Option<serde::de::IgnoredAny>,
#[allow(dead_code)]
managed: Option<serde::de::IgnoredAny>,
#[allow(dead_code)]
r#package: Option<serde::de::IgnoredAny>,
}

Expand Down Expand Up @@ -1616,11 +1632,11 @@ impl From<OptionsWire> for Options {
environments,
publish_url,
trusted_publishing,
workspace: _,
sources: _,
dev_dependencies: _,
managed: _,
package: _,
workspace,
sources,
dev_dependencies,
managed,
package,
} = value;

Self {
Expand Down Expand Up @@ -1664,15 +1680,20 @@ impl From<OptionsWire> for Options {
no_binary,
no_binary_package,
},
publish: PublishOptions {
publish_url,
trusted_publishing,
},
pip,
cache_keys,
override_dependencies,
constraint_dependencies,
environments,
publish: PublishOptions {
publish_url,
trusted_publishing,
},
workspace,
sources,
dev_dependencies,
managed,
package,
}
}
}
Expand Down
16 changes: 8 additions & 8 deletions crates/uv-workspace/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ pub struct ToolUv {
///
/// See [Dependencies](../concepts/dependencies.md) for more.
#[option(
default = "\"[]\"",
default = "{}",
value_type = "dict",
example = r#"
[tool.uv.sources]
Expand Down Expand Up @@ -199,7 +199,7 @@ pub struct ToolUv {
/// given the lowest priority when resolving packages. Additionally, marking an index as default will disable the
/// PyPI default index.
#[option(
default = "\"[]\"",
default = "[]",
value_type = "dict",
example = r#"
[[tool.uv.index]]
Expand Down Expand Up @@ -253,7 +253,7 @@ pub struct ToolUv {
)
)]
#[option(
default = r#"[]"#,
default = "[]",
value_type = "list[str]",
example = r#"
dev-dependencies = ["ruff==0.5.0"]
Expand All @@ -277,7 +277,7 @@ pub struct ToolUv {
)
)]
#[option(
default = r#"[]"#,
default = "[]",
value_type = "str | list[str]",
example = r#"
# Resolve for macOS, but not for Linux or Windows.
Expand Down Expand Up @@ -312,7 +312,7 @@ pub struct ToolUv {
)
)]
#[option(
default = r#"[]"#,
default = "[]",
value_type = "list[str]",
example = r#"
# Always install Werkzeug 2.3.0, regardless of whether transitive dependencies request
Expand Down Expand Up @@ -343,7 +343,7 @@ pub struct ToolUv {
)
)]
#[option(
default = r#"[]"#,
default = "[]",
value_type = "list[str]",
example = r#"
# Ensure that the grpcio version is always less than 1.65, if it's requested by a
Expand Down Expand Up @@ -424,7 +424,7 @@ pub struct ToolUvWorkspace {
///
/// For more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html).
#[option(
default = r#"[]"#,
default = "[]",
value_type = "list[str]",
example = r#"
members = ["member1", "path/to/member2", "libs/*"]
Expand All @@ -438,7 +438,7 @@ pub struct ToolUvWorkspace {
///
/// For more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html).
#[option(
default = r#"[]"#,
default = "[]",
value_type = "list[str]",
example = r#"
exclude = ["member1", "path/to/member2", "libs/*"]
Expand Down
22 changes: 22 additions & 0 deletions crates/uv/tests/it/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,28 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> {
Ok(())
}

#[test]
fn invalid_uv_toml_option_disallowed() -> Result<()> {
let context = TestContext::new("3.12");
let uv_toml = context.temp_dir.child("uv.toml");
uv_toml.write_str(indoc! {r"
managed = true
"})?;

uv_snapshot!(context.pip_install()
.arg("iniconfig"), @r###"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Failed to parse: `uv.toml`. The `managed` field is not allowed in a `uv.toml` file. `managed` is only applicable in the context of a project, and should be placed in a `pyproject.toml` file instead.
"###
);

Ok(())
}

/// For indirect, non-user controlled pyproject.toml, we don't enforce correctness.
///
/// If we fail to extract the PEP 621 metadata, we fall back to treating it as a source
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ If an index is marked as `default = true`, it will be moved to the end of the pr
given the lowest priority when resolving packages. Additionally, marking an index as default will disable the
PyPI default index.

**Default value**: `"[]"`
**Default value**: `[]`

**Type**: `dict`

Expand Down Expand Up @@ -208,7 +208,7 @@ alternative registry.

See [Dependencies](../concepts/dependencies.md) for more.

**Default value**: `"[]"`
**Default value**: `{}`

**Type**: `dict`

Expand Down
Loading