diff --git a/Cargo.lock b/Cargo.lock index 1ef938990..350851e2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4999,6 +4999,7 @@ dependencies = [ "uv-normalize", "uv-resolver", "uv-toolchain", + "uv-warnings", ] [[package]] diff --git a/crates/uv-settings/Cargo.toml b/crates/uv-settings/Cargo.toml index a6e05c7a0..8dbff5fc7 100644 --- a/crates/uv-settings/Cargo.toml +++ b/crates/uv-settings/Cargo.toml @@ -19,10 +19,11 @@ pep508_rs = { workspace = true } pypi-types = { workspace = true } uv-configuration = { workspace = true, features = ["schemars"] } uv-fs = { workspace = true } +uv-macros = { workspace = true } uv-normalize = { workspace = true, features = ["schemars"] } uv-resolver = { workspace = true, features = ["schemars"] } uv-toolchain = { workspace = true, features = ["schemars"] } -uv-macros = { workspace = true } +uv-warnings = { workspace = true } dirs-sys = { workspace = true } fs-err = { workspace = true } diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index a5bfb2f5b..a89176d3c 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -4,6 +4,7 @@ use std::path::{Path, PathBuf}; use tracing::debug; use uv_fs::Simplified; +use uv_warnings::warn_user; pub use crate::combine::*; pub use crate::settings::*; @@ -57,22 +58,25 @@ impl FilesystemOptions { /// Find the [`FilesystemOptions`] for the given path. /// - /// The search starts at the given path and goes up the directory tree until a `uv.toml` file is - /// found. + /// The search starts at the given path and goes up the directory tree until a `uv.toml` file or + /// `pyproject.toml` file is found. pub fn find(path: impl AsRef) -> Result, Error> { for ancestor in path.as_ref().ancestors() { - // Read a `uv.toml` file in the current directory. - let path = ancestor.join("uv.toml"); - 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))?; - - debug!("Found workspace configuration at `{}`", path.display()); - return Ok(Some(Self(options))); + match Self::from_directory(ancestor) { + Ok(Some(options)) => { + return Ok(Some(options)); + } + Ok(None) => { + // Continue traversing the directory tree. + } + Err(Error::PyprojectToml(file, _err)) => { + // If we see an invalid `pyproject.toml`, warn but continue. + warn_user!("Failed to parse `{file}` during settings discovery; skipping..."); + } + Err(err) => { + // Otherwise, warn and stop. + return Err(err); } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) => return Err(err.into()), } } Ok(None) @@ -103,9 +107,17 @@ impl FilesystemOptions { let pyproject: PyProjectToml = toml::from_str(&content) .map_err(|err| Error::PyprojectToml(path.user_display().to_string(), err))?; let Some(tool) = pyproject.tool else { + debug!( + "Skipping `pyproject.toml` in `{}` (no `[tool]` section)", + dir.as_ref().display() + ); return Ok(None); }; let Some(options) = tool.uv else { + debug!( + "Skipping `pyproject.toml` in `{}` (no `[tool.uv]` section)", + dir.as_ref().display() + ); return Ok(None); }; diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index b35d921ea..d272f11c0 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -3204,6 +3204,7 @@ fn override_dependency_from_workspace_invalid_syntax() -> Result<()> { ----- stdout ----- ----- stderr ----- + warning: Failed to parse `pyproject.toml` during settings discovery; skipping... error: Failed to parse: `pyproject.toml` Caused by: TOML parse error at line 9, column 29 | diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 076af4ab9..5d69ec048 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -143,6 +143,7 @@ fn invalid_pyproject_toml_syntax() -> Result<()> { ----- stdout ----- ----- stderr ----- + warning: Failed to parse `pyproject.toml` during settings discovery; skipping... error: Failed to parse: `pyproject.toml` Caused by: TOML parse error at line 1, column 5 | diff --git a/crates/uv/tests/show_settings.rs b/crates/uv/tests/show_settings.rs index ed41c7d00..bbe1df83f 100644 --- a/crates/uv/tests/show_settings.rs +++ b/crates/uv/tests/show_settings.rs @@ -2246,3 +2246,142 @@ fn resolve_user_configuration() -> anyhow::Result<()> { Ok(()) } + +/// Read from a `pyproject.toml` file in the current directory. In this case, the `pyproject.toml` +/// file uses the Poetry schema. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn resolve_poetry_toml() -> anyhow::Result<()> { + let context = TestContext::new("3.12"); + + // Write a `uv.toml` file to the directory. + let config = context.temp_dir.child("pyproject.toml"); + config.write_str(indoc::indoc! {r#" + [tool.poetry] + name = "project" + version = "0.1.0" + + [tool.poetry.dependencies] + python = "^3.10" + rich = "^13.7.1" + + [build-system] + requires = ["poetry-core"] + build-backend = "poetry.core.masonry.api" + + [tool.uv.pip] + resolution = "lowest-direct" + "#})?; + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio>3.0.0")?; + + // Resolution should use the lowest direct version, and generate hashes. + uv_snapshot!(context.filters(), command(&context) + .arg("--show-settings") + .arg("requirements.in"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + quiet: false, + verbose: 0, + color: Auto, + native_tls: false, + connectivity: Online, + isolated: false, + show_settings: true, + preview: Disabled, + toolchain_preference: OnlySystem, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + PipCompileSettings { + src_file: [ + "requirements.in", + ], + constraint: [], + override: [], + overrides_from_workspace: [], + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: PipSettings { + index_locations: IndexLocations { + index: None, + 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, + }, + strict: false, + dependency_mode: Transitive, + resolution: LowestDirect, + prerelease: IfNecessaryOrExplicit, + output_file: None, + no_strip_extras: false, + no_annotate: false, + no_header: false, + custom_compile_command: None, + generate_hashes: false, + setup_py: Pep517, + config_setting: ConfigSettings( + {}, + ), + python_version: None, + python_platform: None, + 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, + require_hashes: false, + upgrade: None, + reinstall: None, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + }, + } + + ----- stderr ----- + "### + ); + + Ok(()) +}