diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index 70ba2f9a6a83d..4690684b5c27d 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -97,3 +97,9 @@ impl Combine for Option { } } } + +impl Combine for serde::de::IgnoredAny { + fn combine(self, _other: Self) -> Self { + self + } +} diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 10552a2102fec..4af608fc63e41 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -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>>, pub constraint_dependencies: Option>>, + + // 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. diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index e6126ab08fbc3..76068511718f8 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -76,10 +76,14 @@ pub struct Tool { pub uv: Option, } +// 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>, /// The workspace definition for the project, if any. #[option_group] diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 4b559022051de..6b8a6473a9b04 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -110,6 +110,12 @@ async fn run(cli: Cli) -> Result { // 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 diff --git a/crates/uv/tests/show_settings.rs b/crates/uv/tests/show_settings.rs index bd4714c5cc162..87227efdc4afc 100644 --- a/crates/uv/tests/show_settings.rs +++ b/crates/uv/tests/show_settings.rs @@ -2680,3 +2680,232 @@ 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. + let config_dir = assert_fs::TempDir::new().expect("Failed to create temp 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: `/var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/[TMP]/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: `/var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/[TMP]/pyproject.toml` + Caused by: TOML parse error at line 9, column 3 + | + 9 | "" + | ^ + expected `.`, `=` + + "### + ); + + Ok(()) +}