Skip to content

Commit f5c86ab

Browse files
charliermarshzaniebkonstin
authored
Error when disallowed settings are defined in uv.toml (#8550)
## Summary These settings can only be defined in `pyproject.toml`, since they're project-centric, and not _configuration_. Closes #8539. --------- Co-authored-by: Zanie Blue <[email protected]> Co-authored-by: Charlie Marsh <[email protected]> Co-authored-by: konsti <[email protected]>
1 parent e4e616f commit f5c86ab

File tree

6 files changed

+119
-37
lines changed

6 files changed

+119
-37
lines changed

crates/uv-settings/src/combine.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::num::NonZeroUsize;
22
use std::path::PathBuf;
3+
34
use url::Url;
45

56
use uv_configuration::{
@@ -124,3 +125,9 @@ impl Combine for serde::de::IgnoredAny {
124125
self
125126
}
126127
}
128+
129+
impl Combine for Option<serde::de::IgnoredAny> {
130+
fn combine(self, _other: Self) -> Self {
131+
self
132+
}
133+
}

crates/uv-settings/src/lib.rs

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ impl FilesystemOptions {
4747
match read_file(&file) {
4848
Ok(options) => {
4949
debug!("Found user configuration in: `{}`", file.display());
50+
validate_uv_toml(&file, &options)?;
5051
Ok(Some(Self(options)))
5152
}
5253
Err(Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
@@ -83,11 +84,11 @@ impl FilesystemOptions {
8384
Ok(None) => {
8485
// Continue traversing the directory tree.
8586
}
86-
Err(Error::PyprojectToml(file, err)) => {
87+
Err(Error::PyprojectToml(path, err)) => {
8788
// If we see an invalid `pyproject.toml`, warn but continue.
8889
warn_user!(
8990
"Failed to parse `{}` during settings discovery:\n{}",
90-
file.cyan(),
91+
path.user_display().cyan(),
9192
textwrap::indent(&err.to_string(), " ")
9293
);
9394
}
@@ -108,7 +109,7 @@ impl FilesystemOptions {
108109
match fs_err::read_to_string(&path) {
109110
Ok(content) => {
110111
let options: Options = toml::from_str(&content)
111-
.map_err(|err| Error::UvToml(path.user_display().to_string(), err))?;
112+
.map_err(|err| Error::UvToml(path.clone(), Box::new(err)))?;
112113

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

127128
debug!("Found workspace configuration at `{}`", path.display());
129+
130+
validate_uv_toml(&path, &options)?;
128131
return Ok(Some(Self(options)));
129132
}
130133
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
@@ -137,7 +140,7 @@ impl FilesystemOptions {
137140
Ok(content) => {
138141
// Parse, but skip any `pyproject.toml` that doesn't have a `[tool.uv]` section.
139142
let pyproject: PyProjectToml = toml::from_str(&content)
140-
.map_err(|err| Error::PyprojectToml(path.user_display().to_string(), err))?;
143+
.map_err(|err| Error::PyprojectToml(path.clone(), Box::new(err)))?;
141144
let Some(tool) = pyproject.tool else {
142145
debug!(
143146
"Skipping `pyproject.toml` in `{}` (no `[tool]` section)",
@@ -238,21 +241,50 @@ fn system_config_file() -> Option<PathBuf> {
238241
/// Load [`Options`] from a `uv.toml` file.
239242
fn read_file(path: &Path) -> Result<Options, Error> {
240243
let content = fs_err::read_to_string(path)?;
241-
let options: Options = toml::from_str(&content)
242-
.map_err(|err| Error::UvToml(path.user_display().to_string(), err))?;
244+
let options: Options =
245+
toml::from_str(&content).map_err(|err| Error::UvToml(path.to_path_buf(), Box::new(err)))?;
243246
Ok(options)
244247
}
245248

249+
/// Validate that an [`Options`] schema is compatible with `uv.toml`.
250+
fn validate_uv_toml(path: &Path, options: &Options) -> Result<(), Error> {
251+
// The `uv.toml` format is not allowed to include any of the following, which are
252+
// permitted by the schema since they _can_ be included in `pyproject.toml` files
253+
// (and we want to use `deny_unknown_fields`).
254+
if options.workspace.is_some() {
255+
return Err(Error::PyprojectOnlyField(path.to_path_buf(), "workspace"));
256+
}
257+
if options.sources.is_some() {
258+
return Err(Error::PyprojectOnlyField(path.to_path_buf(), "sources"));
259+
}
260+
if options.dev_dependencies.is_some() {
261+
return Err(Error::PyprojectOnlyField(
262+
path.to_path_buf(),
263+
"dev-dependencies",
264+
));
265+
}
266+
if options.managed.is_some() {
267+
return Err(Error::PyprojectOnlyField(path.to_path_buf(), "managed"));
268+
}
269+
if options.package.is_some() {
270+
return Err(Error::PyprojectOnlyField(path.to_path_buf(), "package"));
271+
}
272+
Ok(())
273+
}
274+
246275
#[derive(thiserror::Error, Debug)]
247276
pub enum Error {
248277
#[error(transparent)]
249278
Io(#[from] std::io::Error),
250279

251-
#[error("Failed to parse: `{0}`")]
252-
PyprojectToml(String, #[source] toml::de::Error),
280+
#[error("Failed to parse: `{}`", _0.user_display())]
281+
PyprojectToml(PathBuf, #[source] Box<toml::de::Error>),
282+
283+
#[error("Failed to parse: `{}`", _0.user_display())]
284+
UvToml(PathBuf, #[source] Box<toml::de::Error>),
253285

254-
#[error("Failed to parse: `{0}`")]
255-
UvToml(String, #[source] toml::de::Error),
286+
#[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())]
287+
PyprojectOnlyField(PathBuf, &'static str),
256288
}
257289

258290
#[cfg(test)]

crates/uv-settings/src/settings.rs

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ pub struct Options {
8686
cache_keys: Option<Vec<CacheKey>>,
8787

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

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

9697
#[cfg_attr(feature = "schemars", schemars(skip))]
9798
pub environments: Option<SupportedEnvironments>,
99+
100+
// NOTE(charlie): These fields should be kept in-sync with `ToolUv` in
101+
// `crates/uv-workspace/src/pyproject.rs`. The documentation lives on that struct.
102+
// They're only respected in `pyproject.toml` files, and should be rejected in `uv.toml` files.
103+
#[cfg_attr(feature = "schemars", schemars(skip))]
104+
pub workspace: Option<serde::de::IgnoredAny>,
105+
106+
#[cfg_attr(feature = "schemars", schemars(skip))]
107+
pub sources: Option<serde::de::IgnoredAny>,
108+
109+
#[cfg_attr(feature = "schemars", schemars(skip))]
110+
pub dev_dependencies: Option<serde::de::IgnoredAny>,
111+
112+
#[cfg_attr(feature = "schemars", schemars(skip))]
113+
pub managed: Option<serde::de::IgnoredAny>,
114+
115+
#[cfg_attr(feature = "schemars", schemars(skip))]
116+
pub r#package: Option<serde::de::IgnoredAny>,
98117
}
99118

100119
impl Options {
@@ -1551,22 +1570,19 @@ pub struct OptionsWire {
15511570
cache_keys: Option<Vec<CacheKey>>,
15521571

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

15591579
// NOTE(charlie): These fields should be kept in-sync with `ToolUv` in
1560-
// `crates/uv-workspace/src/pyproject.rs`.
1561-
#[allow(dead_code)]
1580+
// `crates/uv-workspace/src/pyproject.rs`. The documentation lives on that struct.
1581+
// They're only respected in `pyproject.toml` files, and should be rejected in `uv.toml` files.
15621582
workspace: Option<serde::de::IgnoredAny>,
1563-
#[allow(dead_code)]
15641583
sources: Option<serde::de::IgnoredAny>,
1565-
#[allow(dead_code)]
15661584
dev_dependencies: Option<serde::de::IgnoredAny>,
1567-
#[allow(dead_code)]
15681585
managed: Option<serde::de::IgnoredAny>,
1569-
#[allow(dead_code)]
15701586
r#package: Option<serde::de::IgnoredAny>,
15711587
}
15721588

@@ -1616,11 +1632,11 @@ impl From<OptionsWire> for Options {
16161632
environments,
16171633
publish_url,
16181634
trusted_publishing,
1619-
workspace: _,
1620-
sources: _,
1621-
dev_dependencies: _,
1622-
managed: _,
1623-
package: _,
1635+
workspace,
1636+
sources,
1637+
dev_dependencies,
1638+
managed,
1639+
package,
16241640
} = value;
16251641

16261642
Self {
@@ -1664,15 +1680,20 @@ impl From<OptionsWire> for Options {
16641680
no_binary,
16651681
no_binary_package,
16661682
},
1667-
publish: PublishOptions {
1668-
publish_url,
1669-
trusted_publishing,
1670-
},
16711683
pip,
16721684
cache_keys,
16731685
override_dependencies,
16741686
constraint_dependencies,
16751687
environments,
1688+
publish: PublishOptions {
1689+
publish_url,
1690+
trusted_publishing,
1691+
},
1692+
workspace,
1693+
sources,
1694+
dev_dependencies,
1695+
managed,
1696+
package,
16761697
}
16771698
}
16781699
}

crates/uv-workspace/src/pyproject.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ pub struct ToolUv {
160160
///
161161
/// See [Dependencies](../concepts/dependencies.md) for more.
162162
#[option(
163-
default = "\"[]\"",
163+
default = "{}",
164164
value_type = "dict",
165165
example = r#"
166166
[tool.uv.sources]
@@ -199,7 +199,7 @@ pub struct ToolUv {
199199
/// given the lowest priority when resolving packages. Additionally, marking an index as default will disable the
200200
/// PyPI default index.
201201
#[option(
202-
default = "\"[]\"",
202+
default = "[]",
203203
value_type = "dict",
204204
example = r#"
205205
[[tool.uv.index]]
@@ -253,7 +253,7 @@ pub struct ToolUv {
253253
)
254254
)]
255255
#[option(
256-
default = r#"[]"#,
256+
default = "[]",
257257
value_type = "list[str]",
258258
example = r#"
259259
dev-dependencies = ["ruff==0.5.0"]
@@ -277,7 +277,7 @@ pub struct ToolUv {
277277
)
278278
)]
279279
#[option(
280-
default = r#"[]"#,
280+
default = "[]",
281281
value_type = "str | list[str]",
282282
example = r#"
283283
# Resolve for macOS, but not for Linux or Windows.
@@ -312,7 +312,7 @@ pub struct ToolUv {
312312
)
313313
)]
314314
#[option(
315-
default = r#"[]"#,
315+
default = "[]",
316316
value_type = "list[str]",
317317
example = r#"
318318
# Always install Werkzeug 2.3.0, regardless of whether transitive dependencies request
@@ -343,7 +343,7 @@ pub struct ToolUv {
343343
)
344344
)]
345345
#[option(
346-
default = r#"[]"#,
346+
default = "[]",
347347
value_type = "list[str]",
348348
example = r#"
349349
# Ensure that the grpcio version is always less than 1.65, if it's requested by a
@@ -424,7 +424,7 @@ pub struct ToolUvWorkspace {
424424
///
425425
/// For more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html).
426426
#[option(
427-
default = r#"[]"#,
427+
default = "[]",
428428
value_type = "list[str]",
429429
example = r#"
430430
members = ["member1", "path/to/member2", "libs/*"]
@@ -438,7 +438,7 @@ pub struct ToolUvWorkspace {
438438
///
439439
/// For more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html).
440440
#[option(
441-
default = r#"[]"#,
441+
default = "[]",
442442
value_type = "list[str]",
443443
example = r#"
444444
exclude = ["member1", "path/to/member2", "libs/*"]

crates/uv/tests/it/pip_install.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,28 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> {
201201
Ok(())
202202
}
203203

204+
#[test]
205+
fn invalid_uv_toml_option_disallowed() -> Result<()> {
206+
let context = TestContext::new("3.12");
207+
let uv_toml = context.temp_dir.child("uv.toml");
208+
uv_toml.write_str(indoc! {r"
209+
managed = true
210+
"})?;
211+
212+
uv_snapshot!(context.pip_install()
213+
.arg("iniconfig"), @r###"
214+
success: false
215+
exit_code: 2
216+
----- stdout -----
217+
218+
----- stderr -----
219+
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.
220+
"###
221+
);
222+
223+
Ok(())
224+
}
225+
204226
/// For indirect, non-user controlled pyproject.toml, we don't enforce correctness.
205227
///
206228
/// If we fail to extract the PEP 621 metadata, we fall back to treating it as a source

docs/reference/settings.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ If an index is marked as `default = true`, it will be moved to the end of the pr
103103
given the lowest priority when resolving packages. Additionally, marking an index as default will disable the
104104
PyPI default index.
105105

106-
**Default value**: `"[]"`
106+
**Default value**: `[]`
107107

108108
**Type**: `dict`
109109

@@ -208,7 +208,7 @@ alternative registry.
208208

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

211-
**Default value**: `"[]"`
211+
**Default value**: `{}`
212212

213213
**Type**: `dict`
214214

0 commit comments

Comments
 (0)