From ba41180447151dbcc8183de4fa9483bbf198181d Mon Sep 17 00:00:00 2001 From: Ahmed Ilyas Date: Thu, 11 Jul 2024 16:40:09 +0200 Subject: [PATCH 1/6] `uv tool run` suggest valid commands when command is not found --- crates/uv/src/commands/tool/run.rs | 61 ++++++++++++++++++++++++++---- crates/uv/tests/tool_run.rs | 40 ++++++++++++++++++++ 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index f9582dd5e363..0951ae0d0e5a 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -1,26 +1,29 @@ use std::borrow::Cow; use std::ffi::OsString; +use std::fmt::Write; use std::path::PathBuf; use std::str::FromStr; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use itertools::Itertools; +use owo_colors::OwoColorize; use tokio::process::Command; use tracing::debug; -use distribution_types::UnresolvedRequirementSpecification; +use distribution_types::{Name, UnresolvedRequirementSpecification}; use pep440_rs::Version; use uv_cache::Cache; use uv_cli::ExternalCommand; use uv_client::{BaseClientBuilder, Connectivity}; use uv_configuration::{Concurrency, PreviewMode}; +use uv_fs::Simplified; use uv_installer::{SatisfiesResult, SitePackages}; use uv_normalize::PackageName; use uv_python::{ EnvironmentPreference, PythonEnvironment, PythonFetch, PythonInstallation, PythonPreference, PythonRequest, }; -use uv_tool::InstalledTools; +use uv_tool::{entrypoint_paths, InstalledTools}; use uv_warnings::warn_user_once; use crate::commands::project::environment::CachedEnvironment; @@ -79,7 +82,6 @@ pub(crate) async fn run( printer, ) .await?; - // TODO(zanieb): Determine the command via the package entry points let command = target; @@ -118,9 +120,54 @@ pub(crate) async fn run( command.to_string_lossy(), args.iter().map(|arg| arg.to_string_lossy()).join(" ") ); - let mut handle = process - .spawn() - .with_context(|| format!("Failed to spawn: `{}`", command.to_string_lossy()))?; + let mut handle = match process.spawn() { + Ok(handle) => Ok(handle), + Err(e) => { + let site_packages = SitePackages::from_environment(&environment) + .context("Failed to read site packages")?; + let package = PackageName::from_str(&from).context("Invalid package name {from}")?; + + let installed = site_packages.get_packages(&package); + let Some(installed_dist) = installed.first().copied() else { + bail!("Expected at least one requirement") + }; + + let entry_points = entrypoint_paths( + &environment, + installed_dist.name(), + installed_dist.version(), + ) + .context("Failed to read entrypoints")?; + + if !entry_points + .iter() + .map(|e| e.0.as_str()) + .any(|e| *e == *command) + { + writeln!( + printer.stdout(), + "The executable {} was not found.", + command.user_display().red() + )?; + if !entry_points.is_empty() { + writeln!( + printer.stdout(), + "However, the following executables are available:", + )?; + } + for entry_point in entry_points { + writeln!( + printer.stdout(), + "- {}", + entry_point.0.user_display().cyan() + )?; + } + }; + Err(e) + } + } + .with_context(|| format!("Failed to spawn: `{}`", command.to_string_lossy()))?; + let status = handle.wait().await.context("Child process disappeared")?; // Exit based on the result of the command diff --git a/crates/uv/tests/tool_run.rs b/crates/uv/tests/tool_run.rs index ccce00dcf961..653c3097382f 100644 --- a/crates/uv/tests/tool_run.rs +++ b/crates/uv/tests/tool_run.rs @@ -148,6 +148,10 @@ fn tool_run_at_version() { success: false exit_code: 2 ----- stdout ----- + The executable pytest@8.0.0 was not found. + However, the following executables are available: + - py.test + - pytest ----- stderr ----- warning: `uv tool run` is experimental and may change without warning. @@ -193,6 +197,42 @@ fn tool_run_from_version() { "###); } +#[test] +fn tool_run_suggest_valid_commands() { + let context = TestContext::new("3.12"); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + uv_snapshot!(context.filters(), context.tool_run() + .arg("--from") + .arg("black") + .arg("orange") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + success: false + exit_code: 2 + ----- stdout ----- + The executable orange was not found. + However, the following executables are available: + - black + - blackd + + ----- stderr ----- + warning: `uv tool run` is experimental and may change without warning. + Resolved 6 packages in [TIME] + Prepared 6 packages in [TIME] + Installed 6 packages in [TIME] + + black==24.3.0 + + click==8.1.7 + + mypy-extensions==1.0.0 + + packaging==24.0 + + pathspec==0.12.1 + + platformdirs==4.2.0 + error: Failed to spawn: `orange` + Caused by: No such file or directory (os error 2) + "###); +} + #[test] fn tool_run_from_install() { let context = TestContext::new("3.12"); From afdfc5e01883857b5450a32a9a696f01b091cb41 Mon Sep 17 00:00:00 2001 From: Ahmed Ilyas Date: Thu, 11 Jul 2024 21:39:00 +0200 Subject: [PATCH 2/6] Windows --- crates/uv/tests/tool_run.rs | 65 +++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/crates/uv/tests/tool_run.rs b/crates/uv/tests/tool_run.rs index 653c3097382f..fafdf44431df 100644 --- a/crates/uv/tests/tool_run.rs +++ b/crates/uv/tests/tool_run.rs @@ -138,7 +138,36 @@ fn tool_run_at_version() { .collect::>(); // When `--from` is used, `@` is not treated as a version request - uv_snapshot!(filters, context.tool_run() + if cfg!(windows) { + uv_snapshot!(filters, context.tool_run() + .arg("--from") + .arg("pytest") + .arg("pytest@8.0.0") + .arg("--version") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + success: false + exit_code: 2 + ----- stdout ----- + The executable pytest@8.0.0 was not found. + However, the following executables are available: + - py.test.exe + - pytest.exe + + ----- stderr ----- + warning: `uv tool run` is experimental and may change without warning. + Resolved 4 packages in [TIME] + Prepared 1 package in [TIME] + Installed 4 packages in [TIME] + + iniconfig==2.0.0 + + packaging==24.0 + + pluggy==1.4.0 + + pytest==8.1.1 + error: Failed to spawn: `pytest@8.0.0` + Caused by: No such file or directory (os error 2) + "###); + } else { + uv_snapshot!(filters, context.tool_run() .arg("--from") .arg("pytest") .arg("pytest@8.0.0") @@ -165,6 +194,7 @@ fn tool_run_at_version() { error: Failed to spawn: `pytest@8.0.0` Caused by: No such file or directory (os error 2) "###); + } } #[test] @@ -203,7 +233,37 @@ fn tool_run_suggest_valid_commands() { let tool_dir = context.temp_dir.child("tools"); let bin_dir = context.temp_dir.child("bin"); - uv_snapshot!(context.filters(), context.tool_run() + if cfg!(windows) { + uv_snapshot!(context.filters(), context.tool_run() + .arg("--from") + .arg("black") + .arg("orange") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + success: false + exit_code: 2 + ----- stdout ----- + The executable orange was not found. + However, the following executables are available: + - black.exe + - blackd.exe + + ----- stderr ----- + warning: `uv tool run` is experimental and may change without warning. + Resolved 6 packages in [TIME] + Prepared 6 packages in [TIME] + Installed 6 packages in [TIME] + + black==24.3.0 + + click==8.1.7 + + mypy-extensions==1.0.0 + + packaging==24.0 + + pathspec==0.12.1 + + platformdirs==4.2.0 + error: Failed to spawn: `orange` + Caused by: program not found + "###); + } else { + uv_snapshot!(context.filters(), context.tool_run() .arg("--from") .arg("black") .arg("orange") @@ -231,6 +291,7 @@ fn tool_run_suggest_valid_commands() { error: Failed to spawn: `orange` Caused by: No such file or directory (os error 2) "###); + } } #[test] From 828a80e4791435cc78af1facf074676045d10961 Mon Sep 17 00:00:00 2001 From: Ahmed Ilyas Date: Thu, 11 Jul 2024 22:44:01 +0200 Subject: [PATCH 3/6] Suggest correct `uv tool run` command in output --- crates/uv/src/commands/tool/run.rs | 2 +- crates/uv/tests/tool_run.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 0951ae0d0e5a..9df4c018a3e5 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -152,7 +152,7 @@ pub(crate) async fn run( if !entry_points.is_empty() { writeln!( printer.stdout(), - "However, the following executables are available:", + "However, the following executables are available: via `uv tool run --from {from} `", )?; } for entry_point in entry_points { diff --git a/crates/uv/tests/tool_run.rs b/crates/uv/tests/tool_run.rs index fafdf44431df..957cc2447b49 100644 --- a/crates/uv/tests/tool_run.rs +++ b/crates/uv/tests/tool_run.rs @@ -150,7 +150,7 @@ fn tool_run_at_version() { exit_code: 2 ----- stdout ----- The executable pytest@8.0.0 was not found. - However, the following executables are available: + However, the following executables are available: via `uv tool run --from pytest ` - py.test.exe - pytest.exe @@ -178,7 +178,7 @@ fn tool_run_at_version() { exit_code: 2 ----- stdout ----- The executable pytest@8.0.0 was not found. - However, the following executables are available: + However, the following executables are available: via `uv tool run --from pytest ` - py.test - pytest @@ -244,7 +244,7 @@ fn tool_run_suggest_valid_commands() { exit_code: 2 ----- stdout ----- The executable orange was not found. - However, the following executables are available: + However, the following executables are available: via `uv tool run --from black ` - black.exe - blackd.exe @@ -273,7 +273,7 @@ fn tool_run_suggest_valid_commands() { exit_code: 2 ----- stdout ----- The executable orange was not found. - However, the following executables are available: + However, the following executables are available: via `uv tool run --from black ` - black - blackd From 33a8875242b0c344df471cb53c5ac25330df7376 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 11 Jul 2024 21:42:29 -0400 Subject: [PATCH 4/6] Small tweaks --- crates/uv/src/commands/tool/run.rs | 81 +++++++++++++---------- crates/uv/tests/tool_run.rs | 100 ++++++++++++----------------- 2 files changed, 88 insertions(+), 93 deletions(-) diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 9df4c018a3e5..4efd9b9de579 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -16,7 +16,6 @@ use uv_cache::Cache; use uv_cli::ExternalCommand; use uv_client::{BaseClientBuilder, Connectivity}; use uv_configuration::{Concurrency, PreviewMode}; -use uv_fs::Simplified; use uv_installer::{SatisfiesResult, SitePackages}; use uv_normalize::PackageName; use uv_python::{ @@ -54,6 +53,8 @@ pub(crate) async fn run( warn_user_once!("`uv tool run` is experimental and may change without warning."); } + let has_from = from.is_some(); + let (target, args) = command.split(); let Some(target) = target else { return Err(anyhow::anyhow!("No tool command provided")); @@ -122,49 +123,42 @@ pub(crate) async fn run( ); let mut handle = match process.spawn() { Ok(handle) => Ok(handle), - Err(e) => { - let site_packages = SitePackages::from_environment(&environment) - .context("Failed to read site packages")?; - let package = PackageName::from_str(&from).context("Invalid package name {from}")?; - - let installed = site_packages.get_packages(&package); - let Some(installed_dist) = installed.first().copied() else { - bail!("Expected at least one requirement") - }; - - let entry_points = entrypoint_paths( - &environment, - installed_dist.name(), - installed_dist.version(), - ) - .context("Failed to read entrypoints")?; - - if !entry_points - .iter() - .map(|e| e.0.as_str()) - .any(|e| *e == *command) - { - writeln!( - printer.stdout(), - "The executable {} was not found.", - command.user_display().red() - )?; - if !entry_points.is_empty() { + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + if let Ok(entrypoints) = get_entrypoints(&from, &environment) { + if entrypoints.is_empty() { writeln!( printer.stdout(), - "However, the following executables are available: via `uv tool run --from {from} `", + "The executable {} was not found.", + command.to_string_lossy().red(), )?; - } - for entry_point in entry_points { + } else { writeln!( printer.stdout(), - "- {}", - entry_point.0.user_display().cyan() + "The executable {} was not found.", + command.to_string_lossy().red() )?; + if has_from { + writeln!( + printer.stdout(), + "However, the following executables are available:", + )?; + } else { + let command = format!("uv tool run --from {from} "); + writeln!( + printer.stdout(), + "However, the following executables are available via {}:", + command.green(), + )?; + } + for (name, _) in entrypoints { + writeln!(printer.stdout(), "- {}", name.cyan())?; + } } + return Ok(ExitStatus::Failure); }; - Err(e) + Err(err) } + Err(err) => Err(err), } .with_context(|| format!("Failed to spawn: `{}`", command.to_string_lossy()))?; @@ -179,6 +173,23 @@ pub(crate) async fn run( } } +/// Return the entry points for the specified package. +fn get_entrypoints(from: &str, environment: &PythonEnvironment) -> Result> { + let site_packages = SitePackages::from_environment(environment)?; + let package = PackageName::from_str(from)?; + + let installed = site_packages.get_packages(&package); + let Some(installed_dist) = installed.first().copied() else { + bail!("Expected at least one requirement") + }; + + Ok(entrypoint_paths( + environment, + installed_dist.name(), + installed_dist.version(), + )?) +} + /// Get or create a [`PythonEnvironment`] in which to run the specified tools. /// /// If the target tool is already installed in a compatible environment, returns that diff --git a/crates/uv/tests/tool_run.rs b/crates/uv/tests/tool_run.rs index 957cc2447b49..b0790ba82ead 100644 --- a/crates/uv/tests/tool_run.rs +++ b/crates/uv/tests/tool_run.rs @@ -174,26 +174,24 @@ fn tool_run_at_version() { .arg("--version") .env("UV_TOOL_DIR", tool_dir.as_os_str()) .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" - success: false - exit_code: 2 - ----- stdout ----- - The executable pytest@8.0.0 was not found. - However, the following executables are available: via `uv tool run --from pytest ` - - py.test - - pytest - - ----- stderr ----- - warning: `uv tool run` is experimental and may change without warning. - Resolved 4 packages in [TIME] - Prepared 1 package in [TIME] - Installed 4 packages in [TIME] - + iniconfig==2.0.0 - + packaging==24.0 - + pluggy==1.4.0 - + pytest==8.1.1 - error: Failed to spawn: `pytest@8.0.0` - Caused by: No such file or directory (os error 2) - "###); + success: false + exit_code: 1 + ----- stdout ----- + The executable pytest@8.0.0 was not found. + However, the following executables are available: + - py.test + - pytest + + ----- stderr ----- + warning: `uv tool run` is experimental and may change without warning. + Resolved 4 packages in [TIME] + Prepared 1 package in [TIME] + Installed 4 packages in [TIME] + + iniconfig==2.0.0 + + packaging==24.0 + + pluggy==1.4.0 + + pytest==8.1.1 + "###); } } @@ -229,24 +227,23 @@ fn tool_run_from_version() { #[test] fn tool_run_suggest_valid_commands() { - let context = TestContext::new("3.12"); + let context = TestContext::new("3.12").with_filtered_exe_suffix(); let tool_dir = context.temp_dir.child("tools"); let bin_dir = context.temp_dir.child("bin"); - if cfg!(windows) { - uv_snapshot!(context.filters(), context.tool_run() - .arg("--from") - .arg("black") - .arg("orange") - .env("UV_TOOL_DIR", tool_dir.as_os_str()) - .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + uv_snapshot!(context.filters(), context.tool_run() + .arg("--from") + .arg("black") + .arg("orange") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- The executable orange was not found. - However, the following executables are available: via `uv tool run --from black ` - - black.exe - - blackd.exe + However, the following executables are available: + - black + - blackd ----- stderr ----- warning: `uv tool run` is experimental and may change without warning. @@ -259,39 +256,26 @@ fn tool_run_suggest_valid_commands() { + packaging==24.0 + pathspec==0.12.1 + platformdirs==4.2.0 - error: Failed to spawn: `orange` - Caused by: program not found "###); - } else { - uv_snapshot!(context.filters(), context.tool_run() - .arg("--from") - .arg("black") - .arg("orange") - .env("UV_TOOL_DIR", tool_dir.as_os_str()) - .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + + uv_snapshot!(context.filters(), context.tool_run() + .arg("fastapi-cli") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- - The executable orange was not found. - However, the following executables are available: via `uv tool run --from black ` - - black - - blackd + The executable fastapi-cli was not found. ----- stderr ----- warning: `uv tool run` is experimental and may change without warning. - Resolved 6 packages in [TIME] - Prepared 6 packages in [TIME] - Installed 6 packages in [TIME] - + black==24.3.0 - + click==8.1.7 - + mypy-extensions==1.0.0 - + packaging==24.0 - + pathspec==0.12.1 - + platformdirs==4.2.0 - error: Failed to spawn: `orange` - Caused by: No such file or directory (os error 2) + Resolved 3 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + fastapi-cli==0.0.1 + + importlib-metadata==1.7.0 + + zipp==3.18.1 "###); - } } #[test] From d5d2a7bb73cfc4668a0712f3e712b98f7b0e5015 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 11 Jul 2024 21:45:25 -0400 Subject: [PATCH 5/6] Small tweaks --- crates/uv/src/commands/tool/run.rs | 53 ++++++++++++++++-------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 4efd9b9de579..c24a35022748 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -8,7 +8,7 @@ use anyhow::{bail, Context, Result}; use itertools::Itertools; use owo_colors::OwoColorize; use tokio::process::Command; -use tracing::debug; +use tracing::{debug, warn}; use distribution_types::{Name, UnresolvedRequirementSpecification}; use pep440_rs::Version; @@ -124,38 +124,43 @@ pub(crate) async fn run( let mut handle = match process.spawn() { Ok(handle) => Ok(handle), Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - if let Ok(entrypoints) = get_entrypoints(&from, &environment) { - if entrypoints.is_empty() { - writeln!( - printer.stdout(), - "The executable {} was not found.", - command.to_string_lossy().red(), - )?; - } else { - writeln!( - printer.stdout(), - "The executable {} was not found.", - command.to_string_lossy().red() - )?; - if has_from { + match get_entrypoints(&from, &environment) { + Ok(entrypoints) => { + if entrypoints.is_empty() { writeln!( printer.stdout(), - "However, the following executables are available:", + "The executable {} was not found.", + command.to_string_lossy().red(), )?; } else { - let command = format!("uv tool run --from {from} "); writeln!( printer.stdout(), - "However, the following executables are available via {}:", - command.green(), + "The executable {} was not found.", + command.to_string_lossy().red() )?; + if has_from { + writeln!( + printer.stdout(), + "However, the following executables are available:", + )?; + } else { + let command = format!("uv tool run --from {from} "); + writeln!( + printer.stdout(), + "However, the following executables are available via {}:", + command.green(), + )?; + } + for (name, _) in entrypoints { + writeln!(printer.stdout(), "- {}", name.cyan())?; + } } - for (name, _) in entrypoints { - writeln!(printer.stdout(), "- {}", name.cyan())?; - } + return Ok(ExitStatus::Failure); + } + Err(err) => { + warn!("Failed to get entrypoints for `{from}`: {err}"); } - return Ok(ExitStatus::Failure); - }; + } Err(err) } Err(err) => Err(err), From 330d7aa595828f32a5de73a1f5f4a44d011ba32a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 11 Jul 2024 22:19:12 -0400 Subject: [PATCH 6/6] Filter exe --- crates/uv/tests/tool_run.rs | 54 +++++++++---------------------------- 1 file changed, 12 insertions(+), 42 deletions(-) diff --git a/crates/uv/tests/tool_run.rs b/crates/uv/tests/tool_run.rs index b0790ba82ead..2d232a81e837 100644 --- a/crates/uv/tests/tool_run.rs +++ b/crates/uv/tests/tool_run.rs @@ -69,7 +69,7 @@ fn tool_run_args() { #[test] fn tool_run_at_version() { - let context = TestContext::new("3.12"); + let context = TestContext::new("3.12").with_filtered_exe_suffix(); let tool_dir = context.temp_dir.child("tools"); let bin_dir = context.temp_dir.child("bin"); @@ -138,21 +138,20 @@ fn tool_run_at_version() { .collect::>(); // When `--from` is used, `@` is not treated as a version request - if cfg!(windows) { - uv_snapshot!(filters, context.tool_run() - .arg("--from") - .arg("pytest") - .arg("pytest@8.0.0") - .arg("--version") - .env("UV_TOOL_DIR", tool_dir.as_os_str()) - .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + uv_snapshot!(filters, context.tool_run() + .arg("--from") + .arg("pytest") + .arg("pytest@8.0.0") + .arg("--version") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- The executable pytest@8.0.0 was not found. - However, the following executables are available: via `uv tool run --from pytest ` - - py.test.exe - - pytest.exe + However, the following executables are available: + - py.test + - pytest ----- stderr ----- warning: `uv tool run` is experimental and may change without warning. @@ -163,36 +162,7 @@ fn tool_run_at_version() { + packaging==24.0 + pluggy==1.4.0 + pytest==8.1.1 - error: Failed to spawn: `pytest@8.0.0` - Caused by: No such file or directory (os error 2) "###); - } else { - uv_snapshot!(filters, context.tool_run() - .arg("--from") - .arg("pytest") - .arg("pytest@8.0.0") - .arg("--version") - .env("UV_TOOL_DIR", tool_dir.as_os_str()) - .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" - success: false - exit_code: 1 - ----- stdout ----- - The executable pytest@8.0.0 was not found. - However, the following executables are available: - - py.test - - pytest - - ----- stderr ----- - warning: `uv tool run` is experimental and may change without warning. - Resolved 4 packages in [TIME] - Prepared 1 package in [TIME] - Installed 4 packages in [TIME] - + iniconfig==2.0.0 - + packaging==24.0 - + pluggy==1.4.0 - + pytest==8.1.1 - "###); - } } #[test]