From 88ece8b7919ab827608bd37efbb1123e5a263d70 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 8 Aug 2024 17:05:02 -0400 Subject: [PATCH] Search beyond workspace root when discovering configuration (#5931) ## Summary Previously, we wouldn't respect configuration files in directories _above_ a workspace root. But this is somewhat problematic, because any `pyproject.toml` will define a workspace root... Instead, I think we should _start_ the search at the workspace root, but go above it if necessary. Closes: #5929. See: https://github.com/astral-sh/uv/pull/4295. --- crates/uv-settings/src/lib.rs | 7 +- crates/uv/src/lib.rs | 14 +- crates/uv/tests/show_settings.rs | 269 +++++++++++++++++++++++++++++++ docs/configuration/files.md | 4 + 4 files changed, 284 insertions(+), 10 deletions(-) diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index 38c5d463e..3ced6e61a 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -40,9 +40,12 @@ impl FilesystemOptions { let root = dir.join("uv"); let file = root.join("uv.toml"); - debug!("Loading user configuration from: `{}`", file.display()); + debug!("Searching for user configuration in: `{}`", file.display()); match read_file(&file) { - Ok(options) => Ok(Some(Self(options))), + Ok(options) => { + debug!("Found user configuration in: `{}`", file.display()); + Ok(Some(Self(options))) + } Err(Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(_) if !dir.is_dir() => { // Ex) `XDG_CONFIG_HOME=/dev/null` diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 99e1d3783..e5f863cf9 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -103,12 +103,10 @@ async fn run(cli: Cli) -> Result { // Load configuration from the filesystem, prioritizing (in order): // 1. The configuration file specified on the command-line. - // 2. The configuration file in the current workspace (i.e., the `pyproject.toml` or `uv.toml` - // file in the workspace root directory). If found, this file is combined with the user - // configuration file. - // 3. The nearest `uv.toml` file in the directory tree, starting from the current directory. If - // 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. + // 2. The nearest configuration file (`uv.toml` or `pyproject.toml`) above the workspace root. + // If found, this file is combined with the user configuration file. + // 3. The nearest configuration file (`uv.toml` or `pyproject.toml`) in the directory tree, + // starting from the current directory. let filesystem = if let Some(config_file) = cli.config_file.as_ref() { if config_file .file_name() @@ -122,8 +120,8 @@ async fn run(cli: Cli) -> Result { } else if matches!(&*cli.command, Commands::Tool(_)) { // For commands that operate at the user-level, ignore local configuration. FilesystemOptions::user()? - } else if let Ok(project) = Workspace::discover(&CWD, &DiscoveryOptions::default()).await { - let project = FilesystemOptions::from_directory(project.install_path())?; + } else if let Ok(workspace) = Workspace::discover(&CWD, &DiscoveryOptions::default()).await { + let project = FilesystemOptions::find(workspace.install_path())?; let user = FilesystemOptions::user()?; project.combine(user) } else { diff --git a/crates/uv/tests/show_settings.rs b/crates/uv/tests/show_settings.rs index 64adb9f27..d7f48fb98 100644 --- a/crates/uv/tests/show_settings.rs +++ b/crates/uv/tests/show_settings.rs @@ -3035,3 +3035,272 @@ fn resolve_config_file() -> anyhow::Result<()> { Ok(()) } + +/// Ignore empty `pyproject.toml` files when discovering configuration. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn resolve_skip_empty() -> anyhow::Result<()> { + let context = TestContext::new("3.12"); + + // Set `lowest-direct` in a `uv.toml`. + let config = context.temp_dir.child("uv.toml"); + config.write_str(indoc::indoc! {r#" + [pip] + resolution = "lowest-direct" + "#})?; + + let child = context.temp_dir.child("child"); + fs_err::create_dir(&child)?; + + // Create an empty in a `pyproject.toml`. + let pyproject = child.child("pyproject.toml"); + pyproject.write_str(indoc::indoc! {r#" + [project] + name = "child" + dependencies = [ + "httpx", + ] + "#})?; + + // Resolution in `child` should use lowest-direct, skipping the `pyproject.toml`, which lacks a + // `tool.uv`. + uv_snapshot!(context.filters(), add_shared_args(context.pip_compile()) + .arg("--show-settings") + .arg("requirements.in") + .current_dir(&child), @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: 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, + 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, + output_file: None, + no_strip_extras: false, + no_strip_markers: 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, + 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 ----- + "### + ); + + // Adding a `tool.uv` section should cause us to ignore the `uv.toml`. + pyproject.write_str(indoc::indoc! {r#" + [project] + name = "child" + dependencies = [ + "httpx", + ] + + [tool.uv] + "#})?; + + uv_snapshot!(context.filters(), add_shared_args(context.pip_compile()) + .arg("--show-settings") + .arg("requirements.in") + .current_dir(&child), @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: 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, + no_build_isolation_package: [], + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + allow_empty_requirements: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + 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: false, + 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 ----- + "### + ); + + Ok(()) +} diff --git a/docs/configuration/files.md b/docs/configuration/files.md index e88e37804..4b7cf4dc2 100644 --- a/docs/configuration/files.md +++ b/docs/configuration/files.md @@ -11,6 +11,10 @@ in the nearest parent directory. files will be ignored. Instead, uv will exclusively read from user-level configuration (e.g., `~/.config/uv/uv.toml`). +In workspaces, uv will begin its search at the workspace root, ignoring any configuration defined in +workspace members. Since the workspace is locked as a single unit, configuration is shared across +all members. + If a `pyproject.toml` file is found, uv will read configuration from the `[tool.uv.pip]` table. For example, to set a persistent index URL, add the following to a `pyproject.toml`: