diff --git a/crates/uv/src/cli.rs b/crates/uv/src/cli.rs index 1d88df8e6..1aafd9214 100644 --- a/crates/uv/src/cli.rs +++ b/crates/uv/src/cli.rs @@ -99,9 +99,13 @@ pub(crate) struct GlobalArgs { /// parent directories. #[arg(global = true, long, hide = true)] pub(crate) isolated: bool, + + /// Show the resolved settings for the current command. + #[arg(global = true, long, hide = true)] + pub(crate) show_settings: bool, } -#[derive(Debug, Clone, clap::ValueEnum)] +#[derive(Debug, Copy, Clone, clap::ValueEnum)] pub(crate) enum ColorChoice { /// Enables colored output only when the output is going to a terminal or TTY with support. Auto, @@ -188,7 +192,7 @@ pub(crate) enum CacheCommand { Dir, } -#[derive(Args)] +#[derive(Args, Debug)] #[allow(clippy::struct_excessive_bools)] pub(crate) struct CleanArgs { /// The packages to remove from the cache. diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index fcb9f5b5b..1896c23ed 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -1,4 +1,5 @@ use std::env; +use std::fmt::Write; use std::io::stdout; use std::path::PathBuf; use std::process::ExitCode; @@ -139,7 +140,10 @@ async fn run() -> Result { }; // Resolve the global settings. - let globals = GlobalSettings::resolve(cli.global_args, filesystem.as_ref()); + let globals = GlobalSettings::resolve(&cli.global_args, filesystem.as_ref()); + + // Resolve the cache settings. + let cache_settings = CacheSettings::resolve(cli.cache_args, filesystem.as_ref()); // Configure the `tracing` crate, which controls internal logging. #[cfg(feature = "tracing-durations-export")] @@ -186,9 +190,25 @@ async fn run() -> Result { debug!("uv {}", version::version()); - // Resolve the cache settings. - let cache = CacheSettings::resolve(cli.cache_args, filesystem.as_ref()); - let cache = Cache::from_settings(cache.no_cache, cache.cache_dir)?; + // Write out any resolved settings. + macro_rules! show_settings { + ($arg:expr) => { + if globals.show_settings { + writeln!(printer.stdout(), "{:#?}", $arg)?; + return Ok(ExitStatus::Success); + } + }; + ($arg:expr, false) => { + if globals.show_settings { + writeln!(printer.stdout(), "{:#?}", $arg)?; + } + }; + } + show_settings!(globals, false); + show_settings!(cache_settings, false); + + // Configure the cache. + let cache = Cache::from_settings(cache_settings.no_cache, cache_settings.cache_dir)?; match cli.command { Commands::Pip(PipNamespace { @@ -198,6 +218,8 @@ async fn run() -> Result { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipCompileSettings::resolve(args, filesystem); + show_settings!(args); + rayon::ThreadPoolBuilder::new() .num_threads(args.settings.concurrency.installs) .build_global() @@ -275,6 +297,8 @@ async fn run() -> Result { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipSyncSettings::resolve(args, filesystem); + show_settings!(args); + rayon::ThreadPoolBuilder::new() .num_threads(args.settings.concurrency.installs) .build_global() @@ -335,6 +359,8 @@ async fn run() -> Result { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipInstallSettings::resolve(args, filesystem); + show_settings!(args); + rayon::ThreadPoolBuilder::new() .num_threads(args.settings.concurrency.installs) .build_global() @@ -410,6 +436,7 @@ async fn run() -> Result { }) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipUninstallSettings::resolve(args, filesystem); + show_settings!(args); // Initialize the cache. let cache = cache.init()?; @@ -445,6 +472,7 @@ async fn run() -> Result { }) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipFreezeSettings::resolve(args, filesystem); + show_settings!(args); // Initialize the cache. let cache = cache.init()?; @@ -466,6 +494,7 @@ async fn run() -> Result { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipListSettings::resolve(args, filesystem); + show_settings!(args); // Initialize the cache. let cache = cache.init()?; @@ -488,6 +517,7 @@ async fn run() -> Result { }) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipShowSettings::resolve(args, filesystem); + show_settings!(args); // Initialize the cache. let cache = cache.init()?; @@ -507,6 +537,7 @@ async fn run() -> Result { }) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipCheckSettings::resolve(args, filesystem); + show_settings!(args); // Initialize the cache. let cache = cache.init()?; @@ -522,7 +553,10 @@ async fn run() -> Result { Commands::Cache(CacheNamespace { command: CacheCommand::Clean(args), }) - | Commands::Clean(args) => commands::cache_clean(&args.package, &cache, printer), + | Commands::Clean(args) => { + show_settings!(args); + commands::cache_clean(&args.package, &cache, printer) + } Commands::Cache(CacheNamespace { command: CacheCommand::Prune, }) => commands::cache_prune(&cache, printer), @@ -537,6 +571,7 @@ async fn run() -> Result { // Resolve the settings from the command-line arguments and workspace configuration. let args = settings::VenvSettings::resolve(args, filesystem); + show_settings!(args); // Initialize the cache. let cache = cache.init()?; @@ -573,6 +608,7 @@ async fn run() -> Result { Commands::Project(ProjectCommand::Run(args)) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = settings::RunSettings::resolve(args, filesystem); + show_settings!(args); // Initialize the cache. let cache = cache.init()?.with_refresh(args.refresh); @@ -605,6 +641,7 @@ async fn run() -> Result { Commands::Project(ProjectCommand::Sync(args)) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = settings::SyncSettings::resolve(args, filesystem); + show_settings!(args); // Initialize the cache. let cache = cache.init()?.with_refresh(args.refresh); @@ -626,6 +663,7 @@ async fn run() -> Result { Commands::Project(ProjectCommand::Lock(args)) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = settings::LockSettings::resolve(args, filesystem); + show_settings!(args); // Initialize the cache. let cache = cache.init()?.with_refresh(args.refresh); @@ -645,6 +683,7 @@ async fn run() -> Result { Commands::Project(ProjectCommand::Add(args)) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = settings::AddSettings::resolve(args, filesystem); + show_settings!(args); // Initialize the cache. let cache = cache.init()?; @@ -664,6 +703,7 @@ async fn run() -> Result { Commands::Project(ProjectCommand::Remove(args)) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = settings::RemoveSettings::resolve(args, filesystem); + show_settings!(args); // Initialize the cache. let cache = cache.init()?; @@ -697,6 +737,7 @@ async fn run() -> Result { }) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = settings::ToolRunSettings::resolve(args, filesystem); + show_settings!(args); // Initialize the cache. let cache = cache.init()?.with_refresh(args.refresh); @@ -723,6 +764,7 @@ async fn run() -> Result { }) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = settings::ToolchainListSettings::resolve(args, filesystem); + show_settings!(args); // Initialize the cache. let cache = cache.init()?; @@ -742,6 +784,7 @@ async fn run() -> Result { }) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = settings::ToolchainInstallSettings::resolve(args, filesystem); + show_settings!(args); // Initialize the cache. let cache = cache.init()?; diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index b83d97591..d303da1a2 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -41,12 +41,13 @@ pub(crate) struct GlobalSettings { pub(crate) native_tls: bool, pub(crate) connectivity: Connectivity, pub(crate) isolated: bool, + pub(crate) show_settings: bool, pub(crate) preview: PreviewMode, } impl GlobalSettings { /// Resolve the [`GlobalSettings`] from the CLI and filesystem configuration. - pub(crate) fn resolve(args: GlobalArgs, workspace: Option<&FilesystemOptions>) -> Self { + pub(crate) fn resolve(args: &GlobalArgs, workspace: Option<&FilesystemOptions>) -> Self { Self { quiet: args.quiet, verbose: args.verbose, @@ -79,6 +80,7 @@ impl GlobalSettings { Connectivity::Online }, isolated: args.isolated, + show_settings: args.show_settings, preview: PreviewMode::from( flag(args.preview, args.no_preview) .combine(workspace.and_then(|workspace| workspace.globals.preview)) diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index d22176e46..31633bd54 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -35,6 +35,9 @@ pub const INSTA_FILTERS: &[(&str, &str)] = &[ (r"(\s|\()(\d+m )?(\d+\.)?\d+(ms|s)", "$1[TIME]"), // File sizes (r"(\s|\()(\d+\.)?\d+([KM]i)?B", "$1[SIZE]"), + // Timestamps + (r"tv_sec: \d+", "tv_sec: [TIME]"), + (r"tv_nsec: \d+", "tv_nsec: [TIME]"), // Rewrite Windows output to Unix output (r"\\([\w\d])", "/$1"), (r"uv.exe", "uv"), diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index f548378c2..cc78fbb42 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -8962,524 +8962,6 @@ fn python_platform() -> Result<()> { Ok(()) } -/// Verify that command-line arguments take precedence over on-disk configuration. -#[test] -fn resolve_configuration() -> 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" - "#})?; - - 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.compile() - .arg("requirements.in"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - anyio==3.0.1 \ - --hash=sha256:1ef7622396ab55829d4236a6f75e2199df6d26a4ba79bea0cb942a5fd2f79a23 \ - --hash=sha256:ed71f7542ef39875b65def219794d9dcb0a48c571317b13612c12b1f292701b5 - # via -r requirements.in - idna==3.6 \ - --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ - --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f - # via anyio - sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via anyio - - ----- stderr ----- - Resolved 3 packages in [TIME] - "### - ); - - // Resolution should use the highest version, and generate hashes. - uv_snapshot!(context.compile() - .arg("requirements.in") - .arg("--resolution=highest"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --resolution=highest - anyio==4.3.0 \ - --hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 \ - --hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6 - # via -r requirements.in - idna==3.6 \ - --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ - --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f - # via anyio - sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via anyio - - ----- stderr ----- - Resolved 3 packages in [TIME] - "### - ); - - // Resolution should use the highest version, and omit hashes. - uv_snapshot!(context.compile() - .arg("requirements.in") - .arg("--resolution=highest") - .arg("--no-generate-hashes"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --resolution=highest --no-generate-hashes - anyio==4.3.0 - # via -r requirements.in - idna==3.6 - # via anyio - sniffio==1.3.1 - # via anyio - - ----- stderr ----- - Resolved 3 packages in [TIME] - "### - ); - - // Write a `pyproject.toml` file to the directory. - let pyproject = context.temp_dir.child("pyproject.toml"); - pyproject.write_str(indoc::indoc! {r#" - [project] - name = "example" - version = "0.0.0" - "#})?; - - // Resolution should use the lowest direct version, and generate hashes. - uv_snapshot!(context.compile() - .arg("requirements.in"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - anyio==3.0.1 \ - --hash=sha256:1ef7622396ab55829d4236a6f75e2199df6d26a4ba79bea0cb942a5fd2f79a23 \ - --hash=sha256:ed71f7542ef39875b65def219794d9dcb0a48c571317b13612c12b1f292701b5 - # via -r requirements.in - idna==3.6 \ - --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ - --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f - # via anyio - sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via anyio - - ----- stderr ----- - Resolved 3 packages in [TIME] - "### - ); - - // Remove the `uv.toml` file. - fs_err::remove_file(config.path())?; - - // Resolution should use the highest version, and omit hashes. - uv_snapshot!(context.compile() - .arg("requirements.in"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - anyio==4.3.0 - # via -r requirements.in - idna==3.6 - # via anyio - sniffio==1.3.1 - # via anyio - - ----- stderr ----- - Resolved 3 packages in [TIME] - "### - ); - - // Add configuration to the `pyproject.toml` file. - pyproject.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" - "#})?; - - // Resolution should use the lowest direct version, and generate hashes. - uv_snapshot!(context.compile() - .arg("requirements.in"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - anyio==3.0.1 \ - --hash=sha256:1ef7622396ab55829d4236a6f75e2199df6d26a4ba79bea0cb942a5fd2f79a23 \ - --hash=sha256:ed71f7542ef39875b65def219794d9dcb0a48c571317b13612c12b1f292701b5 - # via -r requirements.in - idna==3.6 \ - --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ - --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f - # via anyio - sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via anyio - - ----- stderr ----- - Resolved 3 packages in [TIME] - "### - ); - - // Add an extra index URL entry to the `pyproject.toml` file. - pyproject.write_str(indoc::indoc! {r#" - [project] - name = "example" - version = "0.0.0" - - [tool.uv.pip] - index-url = "https://test.pypi.org/simple" - extra-index-url = ["https://pypi.org/simple"] - "#})?; - - // Resolution should succeed, since the PyPI index is preferred. - uv_snapshot!(context.compile() - .arg("requirements.in"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - anyio==4.3.0 - # via -r requirements.in - idna==3.6 - # via anyio - sniffio==1.3.1 - # via anyio - - ----- stderr ----- - Resolved 3 packages in [TIME] - "### - ); - - // Providing an additional index URL on the command-line should fail, since it will be - // preferred (but the test index alone can't satisfy the requirements). - uv_snapshot!(context.compile() - .arg("requirements.in") - .arg("--extra-index-url") - .arg("https://test.pypi.org/simple"), @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only idna<2.8 is available and anyio==3.5.0 depends on idna>=2.8, we can conclude that anyio==3.5.0 cannot be used. - And because only the following versions of anyio are available: - anyio<=3.0.0 - anyio==3.5.0 - and you require anyio, we can conclude that the requirements are unsatisfiable. - "### - ); - - // If we allow the resolver to use _any_ index, it should succeed, since it now has _both_ - // the test and PyPI indexes in its `--extra-index-url`. - uv_snapshot!(context.compile() - .arg("requirements.in") - .arg("--extra-index-url") - .arg("https://test.pypi.org/simple") - .arg("--index-strategy") - .arg("unsafe-best-match"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --index-strategy unsafe-best-match - anyio==4.3.0 - # via -r requirements.in - idna==3.6 - # via anyio - sniffio==1.3.1 - # via anyio - - ----- stderr ----- - Resolved 3 packages in [TIME] - "### - ); - - // Write out a `--find-links` entry. - pyproject.write_str(indoc::indoc! {r#" - [project] - name = "example" - version = "0.0.0" - - [tool.uv.pip] - no-index = true - find-links = ["https://download.pytorch.org/whl/torch_stable.html"] - "#})?; - - let requirements_in = context.temp_dir.child("requirements.in"); - requirements_in.write_str("tqdm")?; - - uv_snapshot!(context.compile() - .arg("requirements.in"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - tqdm==4.66.2 - # via -r requirements.in - - ----- stderr ----- - Resolved 1 package in [TIME] - "### - ); - - // Write out to the top-level (`tool.uv`, rather than `tool.uv.pip`). - pyproject.write_str(indoc::indoc! {r#" - [project] - name = "example" - version = "0.0.0" - - [tool.uv] - resolution = "lowest-direct" - "#})?; - - let requirements_in = context.temp_dir.child("requirements.in"); - requirements_in.write_str("anyio>3.0.0")?; - - uv_snapshot!(context.compile() - .arg("requirements.in"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - anyio==3.0.1 - # via -r requirements.in - idna==3.6 - # via anyio - sniffio==1.3.1 - # via anyio - - ----- stderr ----- - Resolved 3 packages in [TIME] - "### - ); - - // Write out to both the top-level (`tool.uv`) and the pip section (`tool.uv.pip`). The - // `tool.uv.pip` section should take precedence. - pyproject.write_str(indoc::indoc! {r#" - [project] - name = "example" - version = "0.0.0" - - [tool.uv] - resolution = "lowest-direct" - - [tool.uv.pip] - resolution = "highest" - "#})?; - - let requirements_in = context.temp_dir.child("requirements.in"); - requirements_in.write_str("anyio>3.0.0")?; - - uv_snapshot!(context.compile() - .arg("requirements.in"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - anyio==4.3.0 - # via -r requirements.in - idna==3.6 - # via anyio - sniffio==1.3.1 - # via anyio - - ----- stderr ----- - Resolved 3 packages in [TIME] - "### - ); - - // But the command-line should take precedence over both. - uv_snapshot!(context.compile() - .arg("requirements.in") - .arg("--resolution=lowest-direct"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --resolution=lowest-direct - anyio==3.0.1 - # via -r requirements.in - idna==3.6 - # via anyio - sniffio==1.3.1 - # via anyio - - ----- stderr ----- - Resolved 3 packages in [TIME] - "### - ); - - Ok(()) -} - -/// Verify that user configuration is respected. -#[test] -#[cfg(not(windows))] -fn resolve_user_configuration() -> Result<()> { - // Create a temporary directory to store the user configuration. - let xdg = assert_fs::TempDir::new().expect("Failed to create temp dir"); - let uv = xdg.child("uv"); - let config = uv.child("uv.toml"); - config.write_str(indoc::indoc! {r#" - [pip] - resolution = "lowest-direct" - "#})?; - - let context = TestContext::new("3.12"); - - let requirements_in = context.temp_dir.child("requirements.in"); - requirements_in.write_str("anyio>3.0.0")?; - - // Resolution should use the lowest direct version. - uv_snapshot!(context.compile() - .arg("requirements.in") - .env("XDG_CONFIG_HOME", xdg.path()), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - anyio==3.0.1 - # via -r requirements.in - idna==3.6 - # via anyio - sniffio==1.3.1 - # via anyio - - ----- stderr ----- - Resolved 3 packages in [TIME] - "### - ); - - // Add a local configuration to generate hashes. - let config = context.temp_dir.child("uv.toml"); - config.write_str(indoc::indoc! {r" - [pip] - generate-hashes = true - "})?; - - // Resolution should use the lowest direct version and generate hashes. - uv_snapshot!(context.compile() - .arg("requirements.in") - .env("XDG_CONFIG_HOME", xdg.path()), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - anyio==3.0.1 \ - --hash=sha256:1ef7622396ab55829d4236a6f75e2199df6d26a4ba79bea0cb942a5fd2f79a23 \ - --hash=sha256:ed71f7542ef39875b65def219794d9dcb0a48c571317b13612c12b1f292701b5 - # via -r requirements.in - idna==3.6 \ - --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ - --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f - # via anyio - sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via anyio - - ----- stderr ----- - Resolved 3 packages in [TIME] - "### - ); - - // Add a local configuration to override the user configuration. - let config = context.temp_dir.child("uv.toml"); - config.write_str(indoc::indoc! {r#" - [pip] - resolution = "highest" - "#})?; - - // Resolution should use the highest version. - uv_snapshot!(context.compile() - .arg("requirements.in") - .env("XDG_CONFIG_HOME", xdg.path()), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - anyio==4.3.0 - # via -r requirements.in - idna==3.6 - # via anyio - sniffio==1.3.1 - # via anyio - - ----- stderr ----- - Resolved 3 packages in [TIME] - "### - ); - - // However, the user-level `tool.uv.pip` settings override the project-level `tool.uv` settings. - // This is awkward, but we merge the user configuration into the workspace configuration, so - // the resulting configuration has both `tool.uv.pip.resolution` (from the user configuration) - // and `tool.uv.resolution` (from the workspace settings), so we choose the former. - let config = context.temp_dir.child("uv.toml"); - config.write_str(indoc::indoc! {r#" - resolution = "highest" - "#})?; - - // Resolution should use the highest version. - uv_snapshot!(context.compile() - .arg("requirements.in") - .env("XDG_CONFIG_HOME", xdg.path()), @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - anyio==3.0.1 - # via -r requirements.in - idna==3.6 - # via anyio - sniffio==1.3.1 - # via anyio - - ----- stderr ----- - Resolved 3 packages in [TIME] - "### - ); - - Ok(()) -} - /// Resolve a specific source distribution via a Git HTTPS dependency. #[test] #[cfg(feature = "git")] diff --git a/crates/uv/tests/show_settings.rs b/crates/uv/tests/show_settings.rs new file mode 100644 index 000000000..716dede6f --- /dev/null +++ b/crates/uv/tests/show_settings.rs @@ -0,0 +1,2092 @@ +#![cfg(all(feature = "python", feature = "pypi"))] + +use std::process::Command; + +use assert_fs::prelude::*; + +use common::{uv_snapshot, TestContext}; + +mod common; + +/// Create a `pip compile` command, overwriting defaults for any settings that vary based on machine +/// and operating system. +fn command(context: &TestContext) -> Command { + let mut command = context.compile(); + command + .env("UV_LINK_MODE", "clone") + .env("UV_CONCURRENT_DOWNLOADS", "50") + .env("UV_CONCURRENT_BUILDS", "16") + .env("UV_CONCURRENT_INSTALLS", "8"); + command +} + +/// Read from a `uv.toml` file in the current directory. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn resolve_uv_toml() -> 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" + "#})?; + + 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, + } + 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: 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_binary: None, + no_build: None, + no_build_isolation: false, + 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: true, + 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_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 ----- + "### + ); + + // Resolution should use the highest version, and generate hashes. + uv_snapshot!(context.filters(), command(&context) + .arg("--show-settings") + .arg("requirements.in") + .arg("--resolution=highest"), @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, + } + 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: 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_binary: None, + no_build: None, + no_build_isolation: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + prerelease: IfNecessaryOrExplicit, + output_file: None, + no_strip_extras: 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, + exclude_newer: Some( + ExcludeNewer( + 2024-03-25T00:00:00Z, + ), + ), + no_emit_package: [], + emit_index_url: false, + emit_find_links: 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 ----- + "### + ); + + // Resolution should use the highest version, and omit hashes. + uv_snapshot!(context.filters(), command(&context) + .arg("--show-settings") + .arg("requirements.in") + .arg("--resolution=highest") + .arg("--no-generate-hashes"), @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, + } + 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: 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_binary: None, + no_build: None, + no_build_isolation: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + 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_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(()) +} + +/// Read from a `pyproject.toml` file in the current directory. +/// +/// We prefer `uv.toml` when both are present, but respect `pyproject.toml` otherwise. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn resolve_pyproject_toml() -> 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 pyproject = context.temp_dir.child("pyproject.toml"); + pyproject.write_str(indoc::indoc! {r#" + [project] + name = "example" + version = "0.0.0" + "#})?; + + 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, + } + 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: 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_binary: None, + no_build: None, + no_build_isolation: false, + 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: true, + 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_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 ----- + "### + ); + + // Remove the `uv.toml` file. + fs_err::remove_file(config.path())?; + + // Resolution should use the highest version, and omit 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, + } + 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_binary: None, + no_build: None, + no_build_isolation: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + 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_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 ----- + "### + ); + + // Add configuration to the `pyproject.toml` file. + pyproject.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" + "#})?; + + // 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, + } + 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: 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_binary: None, + no_build: None, + no_build_isolation: false, + 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: true, + 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_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(()) +} + +/// Merge index URLs across configuration. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn resolve_index_url() -> anyhow::Result<()> { + let context = TestContext::new("3.12"); + + // Write a `pyproject.toml` file to the directory. + let pyproject = context.temp_dir.child("pyproject.toml"); + pyproject.write_str(indoc::indoc! {r#" + [project] + name = "example" + version = "0.0.0" + + [tool.uv.pip] + index-url = "https://test.pypi.org/simple" + extra-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("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, + } + 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: Some( + Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "test.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://test.pypi.org/simple", + ), + }, + ), + ), + extra_index: [ + 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", + ), + }, + ), + ], + 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_binary: None, + no_build: None, + no_build_isolation: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + 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_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 ----- + "### + ); + + // Providing an additional index URL on the command-line should be merged with the + // configuration file. + uv_snapshot!(context.filters(), command(&context) + .arg("--show-settings") + .arg("requirements.in") + .arg("--extra-index-url") + .arg("https://test.pypi.org/simple"), @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, + } + 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: Some( + Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "test.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://test.pypi.org/simple", + ), + }, + ), + ), + extra_index: [ + Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "test.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://test.pypi.org/simple", + ), + }, + ), + 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", + ), + }, + ), + ], + 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_binary: None, + no_build: None, + no_build_isolation: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + 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_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(()) +} + +/// Allow `--find-links` in configuration files. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn resolve_find_links() -> anyhow::Result<()> { + let context = TestContext::new("3.12"); + + // Write a `pyproject.toml` file to the directory. + let pyproject = context.temp_dir.child("pyproject.toml"); + pyproject.write_str(indoc::indoc! {r#" + [project] + name = "example" + version = "0.0.0" + + [tool.uv.pip] + no-index = true + find-links = ["https://download.pytorch.org/whl/torch_stable.html"] + "#})?; + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("tqdm")?; + + 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, + } + 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: [ + Url( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), + ), + port: None, + path: "/whl/torch_stable.html", + query: None, + fragment: None, + }, + ), + ], + no_index: false, + }, + python: None, + system: false, + extras: None, + break_system_packages: false, + target: None, + prefix: None, + index_strategy: FirstIndex, + keyring_provider: Disabled, + no_binary: None, + no_build: None, + no_build_isolation: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + 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_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(()) +} + +/// Merge configuration between the top-level `tool.uv` and the more specific `tool.uv.pip`. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn resolve_top_level() -> anyhow::Result<()> { + let context = TestContext::new("3.12"); + + // Write out to the top-level (`tool.uv`, rather than `tool.uv.pip`). + let pyproject = context.temp_dir.child("pyproject.toml"); + pyproject.write_str(indoc::indoc! {r#" + [project] + name = "example" + version = "0.0.0" + + [tool.uv] + resolution = "lowest-direct" + "#})?; + + 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("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, + } + 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_binary: None, + no_build: None, + no_build_isolation: false, + 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_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 ----- + "### + ); + + // Write out to both the top-level (`tool.uv`) and the pip section (`tool.uv.pip`). The + // `tool.uv.pip` section should take precedence. + pyproject.write_str(indoc::indoc! {r#" + [project] + name = "example" + version = "0.0.0" + + [tool.uv] + resolution = "lowest-direct" + + [tool.uv.pip] + resolution = "highest" + "#})?; + + 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("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, + } + 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_binary: None, + no_build: None, + no_build_isolation: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + 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_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 ----- + "### + ); + + // But the command-line should take precedence over both. + uv_snapshot!(context.filters(), command(&context) + .arg("--show-settings") + .arg("requirements.in") + .arg("--resolution=lowest-direct"), @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, + } + 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_binary: None, + no_build: None, + no_build_isolation: false, + 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_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(()) +} + +/// Verify that user configuration is respected. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn resolve_user_configuration() -> anyhow::Result<()> { + // Create a temporary directory to store the user configuration. + let xdg = assert_fs::TempDir::new().expect("Failed to create temp dir"); + let uv = xdg.child("uv"); + let config = uv.child("uv.toml"); + config.write_str(indoc::indoc! {r#" + [pip] + resolution = "lowest-direct" + "#})?; + + let context = TestContext::new("3.12"); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio>3.0.0")?; + + // Resolution should use the lowest direct version. + uv_snapshot!(context.filters(), command(&context) + .arg("--show-settings") + .arg("requirements.in") + .env("XDG_CONFIG_HOME", xdg.path()), @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, + } + 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_binary: None, + no_build: None, + no_build_isolation: false, + 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_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 ----- + "### + ); + + // Add a local configuration to generate hashes. + let config = context.temp_dir.child("uv.toml"); + config.write_str(indoc::indoc! {r" + [pip] + generate-hashes = true + "})?; + + // Resolution should use the lowest direct version and generate hashes. + uv_snapshot!(context.filters(), command(&context) + .arg("--show-settings") + .arg("requirements.in") + .env("XDG_CONFIG_HOME", xdg.path()), @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, + } + 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_binary: None, + no_build: None, + no_build_isolation: false, + 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: true, + 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_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 ----- + "### + ); + + // Add a local configuration to override the user configuration. + let config = context.temp_dir.child("uv.toml"); + config.write_str(indoc::indoc! {r#" + [pip] + resolution = "highest" + "#})?; + + // Resolution should use the highest version. + uv_snapshot!(context.filters(), command(&context) + .arg("--show-settings") + .arg("requirements.in") + .env("XDG_CONFIG_HOME", xdg.path()), @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, + } + 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_binary: None, + no_build: None, + no_build_isolation: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + 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_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 ----- + "### + ); + + // However, the user-level `tool.uv.pip` settings override the project-level `tool.uv` settings. + // This is awkward, but we merge the user configuration into the workspace configuration, so + // the resulting configuration has both `tool.uv.pip.resolution` (from the user configuration) + // and `tool.uv.resolution` (from the workspace settings), so we choose the former. + let config = context.temp_dir.child("uv.toml"); + config.write_str(indoc::indoc! {r#" + resolution = "highest" + "#})?; + + // Resolution should use the highest version. + uv_snapshot!(context.filters(), command(&context) + .arg("--show-settings") + .arg("requirements.in") + .env("XDG_CONFIG_HOME", xdg.path()), @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, + } + 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_binary: None, + no_build: None, + no_build_isolation: false, + 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_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(()) +}