diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index 54ae4e261..86e1e0596 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -119,10 +119,9 @@ impl FilesystemOptions { .ok() .and_then(|content| toml::from_str::(&content).ok()) { - if pyproject.tool.is_some_and(|tool| tool.uv.is_some()) { - warn_user!( - "Found both a `uv.toml` file and a `[tool.uv]` section in an adjacent `pyproject.toml`. The `[tool.uv]` section will be ignored in favor of the `uv.toml` file." - ); + if let Some(options) = pyproject.tool.as_ref().and_then(|tool| tool.uv.as_ref()) + { + warn_uv_toml_masked_fields(options); } } @@ -225,6 +224,253 @@ fn validate_uv_toml(path: &Path, options: &Options) -> Result<(), Error> { Ok(()) } +/// Validate that an [`Options`] contains no fields that `uv.toml` would mask +/// +/// This is essentially the inverse of [`validated_uv_toml`][]. +fn warn_uv_toml_masked_fields(options: &Options) { + let Options { + globals: + GlobalOptions { + required_version, + native_tls, + offline, + no_cache, + cache_dir, + preview, + python_preference, + python_downloads, + concurrent_downloads, + concurrent_builds, + concurrent_installs, + allow_insecure_host, + }, + top_level: + ResolverInstallerOptions { + index, + index_url, + extra_index_url, + no_index, + find_links, + index_strategy, + keyring_provider, + resolution, + prerelease, + fork_strategy, + dependency_metadata, + config_settings, + no_build_isolation, + no_build_isolation_package, + exclude_newer, + link_mode, + compile_bytecode, + no_sources, + upgrade, + upgrade_package, + reinstall, + reinstall_package, + no_build, + no_build_package, + no_binary, + no_binary_package, + }, + install_mirrors: + PythonInstallMirrors { + python_install_mirror, + pypy_install_mirror, + python_downloads_json_url, + }, + publish: + PublishOptions { + publish_url, + trusted_publishing, + check_url, + }, + add: AddOptions { add_bounds }, + pip, + cache_keys, + override_dependencies, + constraint_dependencies, + build_constraint_dependencies, + environments, + required_environments, + conflicts: _, + workspace: _, + sources: _, + dev_dependencies: _, + default_groups: _, + dependency_groups: _, + managed: _, + package: _, + build_backend: _, + } = options; + + let mut masked_fields = vec![]; + + if required_version.is_some() { + masked_fields.push("required-version"); + } + if native_tls.is_some() { + masked_fields.push("native-tls"); + } + if offline.is_some() { + masked_fields.push("offline"); + } + if no_cache.is_some() { + masked_fields.push("no-cache"); + } + if cache_dir.is_some() { + masked_fields.push("cache-dir"); + } + if preview.is_some() { + masked_fields.push("preview"); + } + if python_preference.is_some() { + masked_fields.push("python-preference"); + } + if python_downloads.is_some() { + masked_fields.push("python-downloads"); + } + if concurrent_downloads.is_some() { + masked_fields.push("concurrent-downloads"); + } + if concurrent_builds.is_some() { + masked_fields.push("concurrent-builds"); + } + if concurrent_installs.is_some() { + masked_fields.push("concurrent-installs"); + } + if allow_insecure_host.is_some() { + masked_fields.push("allow-insecure-host"); + } + if index.is_some() { + masked_fields.push("index"); + } + if index_url.is_some() { + masked_fields.push("index-url"); + } + if extra_index_url.is_some() { + masked_fields.push("extra-index-url"); + } + if no_index.is_some() { + masked_fields.push("no-index"); + } + if find_links.is_some() { + masked_fields.push("find-links"); + } + if index_strategy.is_some() { + masked_fields.push("index-strategy"); + } + if keyring_provider.is_some() { + masked_fields.push("keyring-provider"); + } + if resolution.is_some() { + masked_fields.push("resolution"); + } + if prerelease.is_some() { + masked_fields.push("prerelease"); + } + if fork_strategy.is_some() { + masked_fields.push("fork-strategy"); + } + if dependency_metadata.is_some() { + masked_fields.push("dependency-metadata"); + } + if config_settings.is_some() { + masked_fields.push("config-settings"); + } + if no_build_isolation.is_some() { + masked_fields.push("no-build-isolation"); + } + if no_build_isolation_package.is_some() { + masked_fields.push("no-build-isolation-package"); + } + if exclude_newer.is_some() { + masked_fields.push("exclude-newer"); + } + if link_mode.is_some() { + masked_fields.push("link-mode"); + } + if compile_bytecode.is_some() { + masked_fields.push("compile-bytecode"); + } + if no_sources.is_some() { + masked_fields.push("no-sources"); + } + if upgrade.is_some() { + masked_fields.push("upgrade"); + } + if upgrade_package.is_some() { + masked_fields.push("upgrade-package"); + } + if reinstall.is_some() { + masked_fields.push("reinstall"); + } + if reinstall_package.is_some() { + masked_fields.push("reinstall-package"); + } + if no_build.is_some() { + masked_fields.push("no-build"); + } + if no_build_package.is_some() { + masked_fields.push("no-build-package"); + } + if no_binary.is_some() { + masked_fields.push("no-binary"); + } + if no_binary_package.is_some() { + masked_fields.push("no-binary-package"); + } + if python_install_mirror.is_some() { + masked_fields.push("python-install-mirror"); + } + if pypy_install_mirror.is_some() { + masked_fields.push("pypy-install-mirror"); + } + if python_downloads_json_url.is_some() { + masked_fields.push("python-downloads-json-url"); + } + if publish_url.is_some() { + masked_fields.push("publish-url"); + } + if trusted_publishing.is_some() { + masked_fields.push("trusted-publishing"); + } + if check_url.is_some() { + masked_fields.push("check-url"); + } + if add_bounds.is_some() { + masked_fields.push("add-bounds"); + } + if pip.is_some() { + masked_fields.push("pip"); + } + if cache_keys.is_some() { + masked_fields.push("cache_keys"); + } + if override_dependencies.is_some() { + masked_fields.push("override-dependencies"); + } + if constraint_dependencies.is_some() { + masked_fields.push("constraint-dependencies"); + } + if build_constraint_dependencies.is_some() { + masked_fields.push("build-constraint-dependencies"); + } + if environments.is_some() { + masked_fields.push("environments"); + } + if required_environments.is_some() { + masked_fields.push("required-environments"); + } + if !masked_fields.is_empty() { + let field_listing = masked_fields.join("\n- "); + warn_user!( + "Found both a `uv.toml` file and a `[tool.uv]` section in an adjacent `pyproject.toml`. The following fields from `[tool.uv]` will be ignored in favor of the `uv.toml` file:\n- {}", + field_listing, + ); + } +} + #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index d80ccce2f..e057cb40a 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -103,7 +103,7 @@ pub struct Options { cache-keys = [{ file = "pyproject.toml" }, { file = "requirements.txt" }, { git = { commit = true } }] "# )] - cache_keys: Option>, + pub cache_keys: Option>, // NOTE(charlie): These fields are shared with `ToolUv` in // `crates/uv-workspace/src/pyproject.rs`. The documentation lives on that struct. diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 7635bd523..53bdddd30 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -3441,6 +3441,8 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { } /// Read from both a `uv.toml` and `pyproject.toml` file in the current directory. +/// +/// Some fields in `[tool.uv]` are masked by `uv.toml` being defined, and should be warned about. #[test] #[cfg_attr( windows, @@ -3465,6 +3467,10 @@ fn resolve_both() -> anyhow::Result<()> { name = "example" version = "0.0.0" + [tool.uv] + offline = true + dev-dependencies = ["pytest"] + [tool.uv.pip] resolution = "highest" extra-index-url = ["https://test.pypi.org/simple"] @@ -3650,7 +3656,232 @@ fn resolve_both() -> anyhow::Result<()> { } ----- stderr ----- - warning: Found both a `uv.toml` file and a `[tool.uv]` section in an adjacent `pyproject.toml`. The `[tool.uv]` section will be ignored in favor of the `uv.toml` file. + warning: Found both a `uv.toml` file and a `[tool.uv]` section in an adjacent `pyproject.toml`. The following fields from `[tool.uv]` will be ignored in favor of the `uv.toml` file: + - offline + - pip + "# + ); + + Ok(()) +} + +/// Read from both a `uv.toml` and `pyproject.toml` file in the current directory. +/// +/// But the fields `[tool.uv]` defines aren't allowed in `uv.toml` so there's no warning. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn resolve_both_special_fields() -> anyhow::Result<()> { + let context = TestContext::new("3.12"); + + // Write a `uv.toml` file to the directory. + let config = context.temp_dir.child("uv.toml"); + config.write_str(indoc::indoc! {r#" + [pip] + resolution = "lowest-direct" + generate-hashes = true + index-url = "https://pypi.org/simple" + "#})?; + + // Write a `pyproject.toml` file to the directory + let config = context.temp_dir.child("pyproject.toml"); + config.write_str(indoc::indoc! {r#" + [project] + name = "example" + version = "0.0.0" + + [dependency-groups] + mygroup = ["iniconfig"] + + [tool.uv] + dev-dependencies = ["pytest"] + + [tool.uv.dependency-groups] + mygroup = {requires-python = ">=3.12"} + "#})?; + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio>3.0.0")?; + + // Resolution should succeed, but warn that the `pip` section in `pyproject.toml` is ignored. + uv_snapshot!(context.filters(), add_shared_args(context.pip_compile(), context.temp_dir.path()) + .arg("--show-settings") + .arg("requirements.in"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + required_version: None, + quiet: 0, + verbose: 0, + color: Auto, + network_settings: NetworkSettings { + connectivity: Online, + native_tls: false, + allow_insecure_host: [], + }, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + show_settings: true, + preview: Disabled, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + installer_metadata: true, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + PipCompileSettings { + format: None, + src_file: [ + "requirements.in", + ], + constraints: [], + overrides: [], + build_constraints: [], + constraints_from_workspace: [], + overrides_from_workspace: [], + build_constraints_from_workspace: [], + environments: SupportedEnvironments( + [], + ), + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: PipSettings { + index_locations: IndexLocations { + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: DisplaySafeUrl { + 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", + ), + }, + ), + explicit: false, + default: true, + origin: None, + format: Simple, + publish_url: None, + authenticate: Auto, + ignore_error_codes: None, + }, + ], + flat_index: [], + no_index: false, + }, + python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + python_downloads_json_url: None, + }, + system: false, + extras: ExtrasSpecification( + ExtrasSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_extras: false, + history: ExtrasSpecificationHistory { + extra: [], + only_extra: [], + no_extra: [], + all_extras: false, + no_default_extras: false, + defaults: List( + [], + ), + }, + }, + ), + groups: [], + break_system_packages: false, + target: None, + prefix: None, + index_strategy: FirstIndex, + keyring_provider: Disabled, + torch_backend: None, + no_build_isolation: false, + no_build_isolation_package: [], + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + allow_empty_requirements: false, + strict: false, + dependency_mode: Transitive, + resolution: LowestDirect, + prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, + dependency_metadata: DependencyMetadata( + {}, + ), + output_file: None, + no_strip_extras: false, + no_strip_markers: false, + no_annotate: false, + no_header: false, + custom_compile_command: None, + generate_hashes: true, + config_setting: ConfigSettings( + {}, + ), + python_version: None, + python_platform: None, + universal: false, + exclude_newer: None, + 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: Some( + Verify, + ), + upgrade: None, + reinstall: None, + }, + } + + ----- stderr ----- "# );