mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-03 21:23:54 +00:00
Reject pyproject.toml in --config-file (#5842)
This already rejects `pyproject.toml`... but because the schema validation is relaxed (we allow unknown fields, and all fields are optional), a `pyproject.toml` doesn't get properly rejected here. This PR makes the schema stricter, but in a safe way (by adding the other `tool.uv` fields, like `workspace`, as any). Closes #5832.
This commit is contained in:
parent
fae9a70ca0
commit
9b06b3905d
6 changed files with 266 additions and 1 deletions
|
|
@ -97,3 +97,9 @@ impl Combine for Option<ConfigSettings> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Combine for serde::de::IgnoredAny {
|
||||
fn combine(self, _other: Self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ pub(crate) struct Tools {
|
|||
/// A `[tool.uv]` section.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Options {
|
||||
#[serde(flatten)]
|
||||
|
|
@ -49,6 +49,24 @@ pub struct Options {
|
|||
)]
|
||||
pub override_dependencies: Option<Vec<Requirement<VerbatimParsedUrl>>>,
|
||||
pub constraint_dependencies: Option<Vec<Requirement<VerbatimParsedUrl>>>,
|
||||
|
||||
// NOTE(charlie): These fields should be kept in-sync with `ToolUv` in
|
||||
// `crates/uv-workspace/src/pyproject.rs`.
|
||||
#[serde(default, skip_serializing)]
|
||||
#[cfg_attr(feature = "schemars", schemars(skip))]
|
||||
workspace: serde::de::IgnoredAny,
|
||||
|
||||
#[serde(default, skip_serializing)]
|
||||
#[cfg_attr(feature = "schemars", schemars(skip))]
|
||||
sources: serde::de::IgnoredAny,
|
||||
|
||||
#[serde(default, skip_serializing)]
|
||||
#[cfg_attr(feature = "schemars", schemars(skip))]
|
||||
dev_dependencies: serde::de::IgnoredAny,
|
||||
|
||||
#[serde(default, skip_serializing)]
|
||||
#[cfg_attr(feature = "schemars", schemars(skip))]
|
||||
managed: serde::de::IgnoredAny,
|
||||
}
|
||||
|
||||
/// Global settings, relevant to all invocations.
|
||||
|
|
|
|||
|
|
@ -76,10 +76,14 @@ pub struct Tool {
|
|||
pub uv: Option<ToolUv>,
|
||||
}
|
||||
|
||||
// NOTE(charlie): When adding fields to this struct, mark them as ignored on `Options` in
|
||||
// `crates/uv-settings/src/settings.rs`.
|
||||
#[derive(Serialize, Deserialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct ToolUv {
|
||||
/// The sources to use (e.g., workspace members, Git repositories, local paths) when resolving
|
||||
/// dependencies.
|
||||
pub sources: Option<BTreeMap<PackageName, Source>>,
|
||||
/// The workspace definition for the project, if any.
|
||||
#[option_group]
|
||||
|
|
|
|||
|
|
@ -110,6 +110,12 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
|||
// found, this file is combined with the user configuration file. In this case, we don't
|
||||
// search for `pyproject.toml` files, since we're not in a workspace.
|
||||
let filesystem = if let Some(config_file) = cli.config_file.as_ref() {
|
||||
if config_file
|
||||
.file_name()
|
||||
.is_some_and(|file_name| file_name == "pyproject.toml")
|
||||
{
|
||||
warn_user!("The `--config-file` argument expects to receive a `uv.toml` file, not a `pyproject.toml`. If you're trying to run a command from another project, use the `--directory` argument instead.");
|
||||
}
|
||||
Some(FilesystemOptions::from_file(config_file)?)
|
||||
} else if deprecated_isolated || cli.no_config {
|
||||
None
|
||||
|
|
|
|||
|
|
@ -2680,3 +2680,233 @@ fn resolve_both() -> anyhow::Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read from a `--config-file` command line argument.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
windows,
|
||||
ignore = "Configuration tests are not yet supported on Windows"
|
||||
)]
|
||||
fn resolve_config_file() -> anyhow::Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Write a `uv.toml` to a temporary location. (Use the cache directory for convenience, since
|
||||
// it's already obfuscated in the fixtures.)
|
||||
let config_dir = &context.cache_dir;
|
||||
let config = config_dir.child("uv.toml");
|
||||
config.write_str(indoc::indoc! {r#"
|
||||
[pip]
|
||||
resolution = "lowest-direct"
|
||||
generate-hashes = true
|
||||
index-url = "https://pypi.org/simple"
|
||||
"#})?;
|
||||
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("anyio>3.0.0")?;
|
||||
|
||||
uv_snapshot!(context.filters(), command(&context)
|
||||
.arg("--show-settings")
|
||||
.arg("--config-file")
|
||||
.arg(config.path())
|
||||
.arg("requirements.in"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
GlobalSettings {
|
||||
quiet: false,
|
||||
verbose: 0,
|
||||
color: Auto,
|
||||
native_tls: false,
|
||||
connectivity: Online,
|
||||
show_settings: true,
|
||||
preview: Disabled,
|
||||
python_preference: OnlySystem,
|
||||
python_fetch: Automatic,
|
||||
no_progress: false,
|
||||
}
|
||||
CacheSettings {
|
||||
no_cache: false,
|
||||
cache_dir: Some(
|
||||
"[CACHE_DIR]/",
|
||||
),
|
||||
}
|
||||
PipCompileSettings {
|
||||
src_file: [
|
||||
"requirements.in",
|
||||
],
|
||||
constraint: [],
|
||||
override: [],
|
||||
constraints_from_workspace: [],
|
||||
overrides_from_workspace: [],
|
||||
build_constraint: [],
|
||||
refresh: None(
|
||||
Timestamp(
|
||||
SystemTime {
|
||||
tv_sec: [TIME],
|
||||
tv_nsec: [TIME],
|
||||
},
|
||||
),
|
||||
),
|
||||
settings: PipSettings {
|
||||
index_locations: IndexLocations {
|
||||
index: Some(
|
||||
Pypi(
|
||||
VerbatimUrl {
|
||||
url: Url {
|
||||
scheme: "https",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: Some(
|
||||
Domain(
|
||||
"pypi.org",
|
||||
),
|
||||
),
|
||||
port: None,
|
||||
path: "/simple",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
given: Some(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
extra_index: [],
|
||||
flat_index: [],
|
||||
no_index: false,
|
||||
},
|
||||
python: None,
|
||||
system: false,
|
||||
extras: None,
|
||||
break_system_packages: false,
|
||||
target: None,
|
||||
prefix: None,
|
||||
index_strategy: FirstIndex,
|
||||
keyring_provider: Disabled,
|
||||
no_build_isolation: false,
|
||||
build_options: BuildOptions {
|
||||
no_binary: None,
|
||||
no_build: None,
|
||||
},
|
||||
allow_empty_requirements: false,
|
||||
strict: false,
|
||||
dependency_mode: Transitive,
|
||||
resolution: LowestDirect,
|
||||
prerelease: IfNecessaryOrExplicit,
|
||||
output_file: None,
|
||||
no_strip_extras: false,
|
||||
no_strip_markers: false,
|
||||
no_annotate: false,
|
||||
no_header: false,
|
||||
custom_compile_command: None,
|
||||
generate_hashes: true,
|
||||
setup_py: Pep517,
|
||||
config_setting: ConfigSettings(
|
||||
{},
|
||||
),
|
||||
python_version: None,
|
||||
python_platform: None,
|
||||
universal: false,
|
||||
exclude_newer: Some(
|
||||
ExcludeNewer(
|
||||
2024-03-25T00:00:00Z,
|
||||
),
|
||||
),
|
||||
no_emit_package: [],
|
||||
emit_index_url: false,
|
||||
emit_find_links: false,
|
||||
emit_build_options: false,
|
||||
emit_marker_expression: false,
|
||||
emit_index_annotation: false,
|
||||
annotation_style: Split,
|
||||
link_mode: Clone,
|
||||
compile_bytecode: false,
|
||||
sources: Enabled,
|
||||
hash_checking: None,
|
||||
upgrade: None,
|
||||
reinstall: None,
|
||||
concurrency: Concurrency {
|
||||
downloads: 50,
|
||||
builds: 16,
|
||||
installs: 8,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
// Write in `pyproject.toml` schema.
|
||||
config.write_str(indoc::indoc! {r#"
|
||||
[project]
|
||||
name = "example"
|
||||
version = "0.0.0"
|
||||
|
||||
[tool.uv.pip]
|
||||
resolution = "lowest-direct"
|
||||
generate-hashes = true
|
||||
index-url = "https://pypi.org/simple"
|
||||
"#})?;
|
||||
|
||||
// The file should be rejected for violating the schema.
|
||||
uv_snapshot!(context.filters(), command(&context)
|
||||
.arg("--show-settings")
|
||||
.arg("--config-file")
|
||||
.arg(config.path())
|
||||
.arg("requirements.in"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Failed to parse: `[CACHE_DIR]/uv.toml`
|
||||
Caused by: TOML parse error at line 1, column 1
|
||||
|
|
||||
1 | [project]
|
||||
| ^
|
||||
unknown field `project`
|
||||
|
||||
"###
|
||||
);
|
||||
|
||||
// Write an _actual_ `pyproject.toml`.
|
||||
let config = config_dir.child("pyproject.toml");
|
||||
config.write_str(indoc::indoc! {r#"
|
||||
[project]
|
||||
name = "example"
|
||||
version = "0.0.0"
|
||||
|
||||
[tool.uv.pip]
|
||||
resolution = "lowest-direct"
|
||||
generate-hashes = true
|
||||
index-url = "https://pypi.org/simple"
|
||||
"""#
|
||||
})?;
|
||||
|
||||
// The file should be rejected for violating the schema, with a custom warning.
|
||||
uv_snapshot!(context.filters(), command(&context)
|
||||
.arg("--show-settings")
|
||||
.arg("--config-file")
|
||||
.arg(config.path())
|
||||
.arg("requirements.in"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: The `--config-file` argument expects to receive a `uv.toml` file, not a `pyproject.toml`. If you're trying to run a command from another project, use the `--directory` argument instead.
|
||||
error: Failed to parse: `[CACHE_DIR]/pyproject.toml`
|
||||
Caused by: TOML parse error at line 9, column 3
|
||||
|
|
||||
9 | ""
|
||||
| ^
|
||||
expected `.`, `=`
|
||||
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
1
uv.schema.json
generated
1
uv.schema.json
generated
|
|
@ -295,6 +295,7 @@
|
|||
]
|
||||
},
|
||||
"sources": {
|
||||
"description": "The sources to use (e.g., workspace members, Git repositories, local paths) when resolving dependencies.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue