From 240edf697ec863c4e0e3b77d3f4e1b6b06cffbf7 Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Sat, 9 Mar 2024 13:48:29 -0500 Subject: [PATCH 1/6] implement "Requires" field in pip show command --- crates/uv/src/commands/pip_show.rs | 14 ++++ crates/uv/tests/pip_show.rs | 124 +++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) diff --git a/crates/uv/src/commands/pip_show.rs b/crates/uv/src/commands/pip_show.rs index 2ed27248b26a..cc5baddf1a57 100644 --- a/crates/uv/src/commands/pip_show.rs +++ b/crates/uv/src/commands/pip_show.rs @@ -62,6 +62,9 @@ pub(crate) fn pip_show( // Build the installed index. let site_packages = SitePackages::from_executable(&venv)?; + // Determine the markers to use for resolution. + let markers = venv.interpreter().markers(); + // Sort and deduplicate the packages, which are keyed by name. packages.sort_unstable(); packages.dedup(); @@ -116,6 +119,17 @@ pub(crate) fn pip_show( .expect("package path is not root") .simplified_display() )?; + + let mut requires = distribution + .metadata() + .unwrap() + .requires_dist + .into_iter() + .filter(|req| req.evaluate_markers(markers, &[])) + .map(|req| req.name.to_string()) + .collect::>(); + requires.sort(); + writeln!(printer.stdout(), "Requires: {}", requires.join(", "))?; } // Validate that the environment is consistent. diff --git a/crates/uv/tests/pip_show.rs b/crates/uv/tests/pip_show.rs index 2b2c0d552a70..151a629535b3 100644 --- a/crates/uv/tests/pip_show.rs +++ b/crates/uv/tests/pip_show.rs @@ -56,6 +56,125 @@ fn show_empty() { ); } +#[test] +fn show_requires_multiple() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.touch()?; + requirements_txt.write_str("requests==2.31.0")?; + + uv_snapshot!(install_command(&context) + .arg("-r") + .arg("requirements.txt") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + Downloaded 5 packages in [TIME] + Installed 5 packages in [TIME] + + certifi==2023.11.17 + + charset-normalizer==3.3.2 + + idna==3.4 + + requests==2.31.0 + + urllib3==2.1.0 + "### + ); + + context.assert_command("import requests").success(); + let filters = [( + r"Location:.*site-packages", + "Location: [WORKSPACE_DIR]/site-packages", + )] + .to_vec(); + + // Guards against the package names being sorted. + uv_snapshot!(filters, Command::new(get_bin()) + .arg("pip") + .arg("show") + .arg("requests") + .arg("--cache-dir") + .arg(context.cache_dir.path()) + .env("VIRTUAL_ENV", context.venv.as_os_str()) + .current_dir(&context.temp_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Name: requests + Version: 2.31.0 + Location: [WORKSPACE_DIR]/site-packages + Requires: certifi, charset-normalizer, idna, urllib3 + + ----- stderr ----- + "### + ); + + Ok(()) +} + +#[test] +fn show_python_version_marker() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.touch()?; + // requires different numpy versions depending on the python version used. + requirements_txt.write_str("pandas==2.1.3")?; + + uv_snapshot!(install_command(&context) + .arg("-r") + .arg("requirements.txt") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Downloaded 6 packages in [TIME] + Installed 6 packages in [TIME] + + numpy==1.26.2 + + pandas==2.1.3 + + python-dateutil==2.8.2 + + pytz==2023.3.post1 + + six==1.16.0 + + tzdata==2023.3 + "### + ); + + context.assert_command("import pandas").success(); + let filters = [( + r"Location:.*site-packages", + "Location: [WORKSPACE_DIR]/site-packages", + )] + .to_vec(); + + uv_snapshot!(filters, Command::new(get_bin()) + .arg("pip") + .arg("show") + .arg("pandas") + .arg("--cache-dir") + .arg(context.cache_dir.path()) + .env("VIRTUAL_ENV", context.venv.as_os_str()) + .current_dir(&context.temp_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Name: pandas + Version: 2.1.3 + Location: [WORKSPACE_DIR]/site-packages + Requires: numpy, python-dateutil, pytz, tzdata + + ----- stderr ----- + "### + ); + + Ok(()) +} + #[test] fn show_found_single_package() -> Result<()> { let context = TestContext::new("3.12"); @@ -101,6 +220,7 @@ fn show_found_single_package() -> Result<()> { Name: markupsafe Version: 2.1.3 Location: [WORKSPACE_DIR]/site-packages + Requires: ----- stderr ----- "### @@ -162,10 +282,12 @@ fn show_found_multiple_packages() -> Result<()> { Name: markupsafe Version: 2.1.3 Location: [WORKSPACE_DIR]/site-packages + Requires: --- Name: pip Version: 21.3.1 Location: [WORKSPACE_DIR]/site-packages + Requires: ----- stderr ----- "### @@ -227,6 +349,7 @@ fn show_found_one_out_of_two() -> Result<()> { Name: markupsafe Version: 2.1.3 Location: [WORKSPACE_DIR]/site-packages + Requires: ----- stderr ----- warning: Package(s) not found for: flask @@ -378,6 +501,7 @@ fn show_editable() -> Result<()> { Name: poetry-editable Version: 0.1.0 Location: [WORKSPACE_DIR]/site-packages + Requires: numpy ----- stderr ----- "### From 5f0e64166525ca3f5ed248851daa76ed0b2cb7b3 Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Sun, 10 Mar 2024 18:47:25 -0400 Subject: [PATCH 2/6] retrigger CI From 40509e87296a511d01968ffdab016096998cf2cd Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Sun, 10 Mar 2024 19:32:29 -0400 Subject: [PATCH 3/6] skip show_python_version_marker test for windows --- crates/uv/tests/pip_show.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/uv/tests/pip_show.rs b/crates/uv/tests/pip_show.rs index 151a629535b3..fb6765b2d6bd 100644 --- a/crates/uv/tests/pip_show.rs +++ b/crates/uv/tests/pip_show.rs @@ -116,6 +116,7 @@ fn show_requires_multiple() -> Result<()> { } #[test] +#[cfg(not(windows))] fn show_python_version_marker() -> Result<()> { let context = TestContext::new("3.12"); From bb6b7bd4971c0ccc3b97a825b1676a26a6e3e115 Mon Sep 17 00:00:00 2001 From: chan Date: Mon, 11 Mar 2024 07:39:22 -0400 Subject: [PATCH 4/6] use click instead of pandas for test --- Cargo.lock | 14 +- Cargo.toml | 2 +- crates/requirements-txt/Cargo.toml | 5 - crates/requirements-txt/src/lib.rs | 168 +++++++++++++---------- crates/uv-interpreter/src/interpreter.rs | 8 +- crates/uv-resolver/src/resolver/mod.rs | 6 +- crates/uv/Cargo.toml | 2 +- crates/uv/src/commands/pip_compile.rs | 15 +- crates/uv/src/commands/pip_install.rs | 19 ++- crates/uv/src/commands/pip_sync.rs | 15 +- crates/uv/src/commands/pip_uninstall.rs | 9 +- crates/uv/src/logging.rs | 139 +++++++++++++++++-- crates/uv/src/main.rs | 19 +-- crates/uv/src/requirements.rs | 28 ++-- crates/uv/tests/pip_install.rs | 3 +- crates/uv/tests/pip_show.rs | 33 ++--- 16 files changed, 313 insertions(+), 172 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3898d12ea70..ae1d4ee6b17f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2816,7 +2816,6 @@ dependencies = [ "pep508_rs", "regex", "reqwest", - "reqwest-middleware", "serde", "serde_json", "tempfile", @@ -3970,6 +3969,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.18" @@ -3980,12 +3989,15 @@ dependencies = [ "nu-ansi-term 0.46.0", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index dd50ebb381cd..1ee58fe71b85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,7 @@ toml = { version = "0.8.8" } tracing = { version = "0.1.40" } tracing-durations-export = { version = "0.2.0", features = ["plot"] } tracing-indicatif = { version = "0.3.6" } -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] } tracing-tree = { version = "0.3.0" } unicode-width = { version = "0.1.11" } unscanny = { version = "0.1.0" } diff --git a/crates/requirements-txt/Cargo.toml b/crates/requirements-txt/Cargo.toml index d98249995fac..c61ba4f37e42 100644 --- a/crates/requirements-txt/Cargo.toml +++ b/crates/requirements-txt/Cargo.toml @@ -25,7 +25,6 @@ fs-err = { workspace = true } once_cell = { workspace = true } regex = { workspace = true } reqwest = { workspace = true, optional = true } -reqwest-middleware = { workspace = true, optional = true } serde = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } @@ -42,7 +41,3 @@ serde_json = { version = "1.0.114" } tempfile = { version = "3.9.0" } test-case = { version = "3.3.1" } tokio = { version = "1.35.1", features = ["macros"] } - -[features] -default = [] -reqwest = ["dep:reqwest", "dep:reqwest-middleware"] diff --git a/crates/requirements-txt/src/lib.rs b/crates/requirements-txt/src/lib.rs index ca3d0935d63d..9506af352e0d 100644 --- a/crates/requirements-txt/src/lib.rs +++ b/crates/requirements-txt/src/lib.rs @@ -49,7 +49,7 @@ use pep508_rs::{ expand_path_vars, split_scheme, Extras, Pep508Error, Pep508ErrorSource, Requirement, Scheme, VerbatimUrl, }; -use uv_client::RegistryClient; +use uv_client::Connectivity; use uv_fs::{normalize_url_path, Simplified}; use uv_normalize::ExtraName; use uv_warnings::warn_user; @@ -326,7 +326,7 @@ impl RequirementsTxt { pub async fn parse( requirements_txt: impl AsRef, working_dir: impl AsRef, - client: Option<&RegistryClient>, + connectivity: Connectivity, ) -> Result { let requirements_txt = requirements_txt.as_ref(); let working_dir = working_dir.as_ref(); @@ -346,17 +346,18 @@ impl RequirementsTxt { #[cfg(feature = "reqwest")] { - let Some(client) = client else { - return Err(RequirementsTxtFileError { - file: requirements_txt.to_path_buf(), - error: RequirementsTxtParserError::IO(io::Error::new( - io::ErrorKind::InvalidInput, - "No client provided for remote file", - )), - }); - }; - - read_url_to_string(&requirements_txt, client).await + match connectivity { + Connectivity::Online => read_url_to_string(&requirements_txt).await, + Connectivity::Offline => { + return Err(RequirementsTxtFileError { + file: requirements_txt.to_path_buf(), + error: RequirementsTxtParserError::IO(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Network connectivity is disabled, but a remote requirements file was requested: {}", requirements_txt.display()), + )), + }); + } + } } } else { uv_fs::read_to_string_transcode(&requirements_txt) @@ -369,7 +370,7 @@ impl RequirementsTxt { })?; let requirements_dir = requirements_txt.parent().unwrap_or(working_dir); - let data = Self::parse_inner(&content, working_dir, requirements_dir, client) + let data = Self::parse_inner(&content, working_dir, requirements_dir, connectivity) .await .map_err(|err| RequirementsTxtFileError { file: requirements_txt.to_path_buf(), @@ -396,7 +397,7 @@ impl RequirementsTxt { content: &str, working_dir: &Path, requirements_dir: &Path, - client: Option<&'async_recursion RegistryClient>, + connectivity: Connectivity, ) -> Result { let mut s = Scanner::new(content); @@ -415,7 +416,7 @@ impl RequirementsTxt { } else { requirements_dir.join(filename.as_ref()) }; - let sub_requirements = Self::parse(&sub_file, working_dir, client) + let sub_requirements = Self::parse(&sub_file, working_dir, connectivity) .await .map_err(|err| RequirementsTxtParserError::Subfile { source: Box::new(err), @@ -453,13 +454,13 @@ impl RequirementsTxt { } else { requirements_dir.join(filename.as_ref()) }; - let sub_constraints = Self::parse(&sub_file, working_dir, client) + let sub_constraints = Self::parse(&sub_file, working_dir, connectivity) .await .map_err(|err| RequirementsTxtParserError::Subfile { - source: Box::new(err), - start, - end, - })?; + source: Box::new(err), + start, + end, + })?; // Treat any nested requirements or constraints as constraints. This differs // from `pip`, which seems to treat `-r` requirements in constraints files as // _requirements_, but we don't want to support that. @@ -819,10 +820,7 @@ fn parse_value<'a, T>( /// Fetch the contents of a URL and return them as a string. #[cfg(feature = "reqwest")] -async fn read_url_to_string( - path: impl AsRef, - client: &RegistryClient, -) -> Result { +async fn read_url_to_string(path: impl AsRef) -> Result { // pip would URL-encode the non-UTF-8 bytes of the string; we just don't support them. let path_utf8 = path.as_ref() @@ -830,17 +828,11 @@ async fn read_url_to_string( .ok_or_else(|| RequirementsTxtParserError::NonUnicodeUrl { url: path.as_ref().to_owned(), })?; - Ok(client - .cached_client() - .uncached() - .get(path_utf8) - .send() + Ok(reqwest::get(path_utf8) .await? - .error_for_status() - .map_err(reqwest_middleware::Error::Reqwest)? + .error_for_status()? .text() - .await - .map_err(reqwest_middleware::Error::Reqwest)?) + .await?) } /// Error parsing requirements.txt, wrapper with filename @@ -888,7 +880,7 @@ pub enum RequirementsTxtParserError { url: PathBuf, }, #[cfg(feature = "reqwest")] - Reqwest(reqwest_middleware::Error), + Reqwest(reqwest::Error), } impl RequirementsTxtParserError { @@ -1119,8 +1111,8 @@ impl From for RequirementsTxtParserError { } #[cfg(feature = "reqwest")] -impl From for RequirementsTxtParserError { - fn from(err: reqwest_middleware::Error) -> Self { +impl From for RequirementsTxtParserError { + fn from(err: reqwest::Error) -> Self { Self::Reqwest(err) } } @@ -1177,6 +1169,7 @@ mod test { use tempfile::tempdir; use test_case::test_case; use unscanny::Scanner; + use uv_client::Connectivity; use uv_fs::Simplified; @@ -1201,7 +1194,7 @@ mod test { let working_dir = workspace_test_data_dir().join("requirements-txt"); let requirements_txt = working_dir.join(path); - let actual = RequirementsTxt::parse(requirements_txt, &working_dir, None) + let actual = RequirementsTxt::parse(requirements_txt, &working_dir, Connectivity::Offline) .await .unwrap(); @@ -1245,7 +1238,7 @@ mod test { let requirements_txt = temp_dir.path().join(path); fs::write(&requirements_txt, contents).unwrap(); - let actual = RequirementsTxt::parse(&requirements_txt, &working_dir, None) + let actual = RequirementsTxt::parse(&requirements_txt, &working_dir, Connectivity::Offline) .await .unwrap(); @@ -1262,9 +1255,13 @@ mod test { -r missing.txt "})?; - let error = RequirementsTxt::parse(requirements_txt.path(), temp_dir.path(), None) - .await - .unwrap_err(); + let error = RequirementsTxt::parse( + requirements_txt.path(), + temp_dir.path(), + Connectivity::Offline, + ) + .await + .unwrap_err(); let errors = anyhow::Error::new(error) .chain() // The last error is operating-system specific. @@ -1299,9 +1296,13 @@ mod test { numpy[รถ]==1.29 "})?; - let error = RequirementsTxt::parse(requirements_txt.path(), temp_dir.path(), None) - .await - .unwrap_err(); + let error = RequirementsTxt::parse( + requirements_txt.path(), + temp_dir.path(), + Connectivity::Offline, + ) + .await + .unwrap_err(); let errors = anyhow::Error::new(error).chain().join("\n"); let requirement_txt = @@ -1332,9 +1333,13 @@ mod test { -e http://localhost:8080/ "})?; - let error = RequirementsTxt::parse(requirements_txt.path(), temp_dir.path(), None) - .await - .unwrap_err(); + let error = RequirementsTxt::parse( + requirements_txt.path(), + temp_dir.path(), + Connectivity::Offline, + ) + .await + .unwrap_err(); let errors = anyhow::Error::new(error).chain().join("\n"); let requirement_txt = @@ -1360,9 +1365,13 @@ mod test { -e black[,abcdef] "})?; - let error = RequirementsTxt::parse(requirements_txt.path(), temp_dir.path(), None) - .await - .unwrap_err(); + let error = RequirementsTxt::parse( + requirements_txt.path(), + temp_dir.path(), + Connectivity::Offline, + ) + .await + .unwrap_err(); let errors = anyhow::Error::new(error).chain().join("\n"); let requirement_txt = @@ -1390,9 +1399,13 @@ mod test { --index-url 123 "})?; - let error = RequirementsTxt::parse(requirements_txt.path(), temp_dir.path(), None) - .await - .unwrap_err(); + let error = RequirementsTxt::parse( + requirements_txt.path(), + temp_dir.path(), + Connectivity::Offline, + ) + .await + .unwrap_err(); let errors = anyhow::Error::new(error).chain().join("\n"); let requirement_txt = @@ -1426,9 +1439,13 @@ mod test { file.txt "})?; - let error = RequirementsTxt::parse(requirements_txt.path(), temp_dir.path(), None) - .await - .unwrap_err(); + let error = RequirementsTxt::parse( + requirements_txt.path(), + temp_dir.path(), + Connectivity::Offline, + ) + .await + .unwrap_err(); let errors = anyhow::Error::new(error).chain().join("\n"); let requirement_txt = @@ -1469,9 +1486,10 @@ mod test { -r subdir/child.txt "})?; - let requirements = RequirementsTxt::parse(parent_txt.path(), temp_dir.path(), None) - .await - .unwrap(); + let requirements = + RequirementsTxt::parse(parent_txt.path(), temp_dir.path(), Connectivity::Offline) + .await + .unwrap(); insta::assert_debug_snapshot!(requirements, @r###" RequirementsTxt { requirements: [ @@ -1521,9 +1539,13 @@ mod test { --no-index "})?; - let requirements = RequirementsTxt::parse(requirements_txt.path(), temp_dir.path(), None) - .await - .unwrap(); + let requirements = RequirementsTxt::parse( + requirements_txt.path(), + temp_dir.path(), + Connectivity::Offline, + ) + .await + .unwrap(); insta::assert_debug_snapshot!(requirements, @r###" RequirementsTxt { @@ -1581,9 +1603,13 @@ mod test { --index-url https://fake.pypi.org/simple "})?; - let error = RequirementsTxt::parse(requirements_txt.path(), temp_dir.path(), None) - .await - .unwrap_err(); + let error = RequirementsTxt::parse( + requirements_txt.path(), + temp_dir.path(), + Connectivity::Offline, + ) + .await + .unwrap_err(); let errors = anyhow::Error::new(error).chain().join("\n"); let requirement_txt = @@ -1631,9 +1657,13 @@ mod test { tqdm "})?; - let error = RequirementsTxt::parse(requirements_txt.path(), temp_dir.path(), None) - .await - .unwrap_err(); + let error = RequirementsTxt::parse( + requirements_txt.path(), + temp_dir.path(), + Connectivity::Offline, + ) + .await + .unwrap_err(); let errors = anyhow::Error::new(error).chain().join("\n"); let requirement_txt = diff --git a/crates/uv-interpreter/src/interpreter.rs b/crates/uv-interpreter/src/interpreter.rs index 310a7d367977..6ff3c326d7d5 100644 --- a/crates/uv-interpreter/src/interpreter.rs +++ b/crates/uv-interpreter/src/interpreter.rs @@ -16,7 +16,7 @@ use platform_host::Platform; use platform_tags::{Tags, TagsError}; use pypi_types::Scheme; use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness, Timestamp}; -use uv_fs::write_atomic_sync; +use uv_fs::{write_atomic_sync, Simplified}; use crate::Error; use crate::Virtualenv; @@ -453,20 +453,20 @@ impl InterpreterInfo { debug!( "Cached interpreter info for Python {}, skipping probing: {}", cached.data.markers.python_full_version, - executable.display() + executable.simplified_display() ); return Ok(cached.data); } debug!( "Ignoring stale cached markers for: {}", - executable.display() + executable.simplified_display() ); } Err(err) => { warn!( "Broken cache entry at {}, removing: {err}", - cache_entry.path().display() + cache_entry.path().simplified_display() ); let _ = fs_err::remove_file(cache_entry.path()); } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 8decdf5407dd..1e1e3283e88d 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -809,11 +809,11 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { self.markers, )?; - for (package, version) in constraints.iter() { - debug!("Adding transitive dependency: {package}{version}"); + for (dep_package, dep_version) in constraints.iter() { + debug!("Adding transitive dependency for {package}{version}: {dep_package}{dep_version}"); // Emit a request to fetch the metadata for this package. - self.visit_package(package, priorities, request_sink) + self.visit_package(dep_package, priorities, request_sink) .await?; } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 296983f2bffb..fa623cae742f 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -67,7 +67,7 @@ tokio = { workspace = true } toml = { workspace = true } tracing = { workspace = true } tracing-durations-export = { workspace = true, features = ["plot"], optional = true } -tracing-subscriber = { workspace = true } +tracing-subscriber = { workspace = true, features = ["json"] } tracing-tree = { workspace = true } unicode-width = { workspace = true } url = { workspace = true } diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index 339f25a0667f..fa255357ba50 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -84,11 +84,6 @@ pub(crate) async fn pip_compile( )); } - // Initialize the registry client. - let client = RegistryClientBuilder::new(cache.clone()) - .connectivity(connectivity) - .build(); - // Read all requirements from the provided sources. let RequirementsSpecification { project, @@ -106,7 +101,7 @@ pub(crate) async fn pip_compile( constraints, overrides, &extras, - &client, + connectivity, ) .await?; @@ -191,9 +186,11 @@ pub(crate) async fn pip_compile( let index_locations = index_locations.combine(index_url, extra_index_urls, find_links, no_index); - // Update the index URLs on the client, to take into account any index URLs added by the - // sources (e.g., `--index-url` in a `requirements.txt` file). - let client = client.with_index_url(index_locations.index_urls()); + // Initialize the registry client. + let client = RegistryClientBuilder::new(cache.clone()) + .connectivity(connectivity) + .index_urls(index_locations.index_urls()) + .build(); // Resolve the flat indexes from `--find-links`. let flat_index = { diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index cd6bd75fd2fe..a122db17dd31 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -72,11 +72,6 @@ pub(crate) async fn pip_install( ) -> Result { let start = std::time::Instant::now(); - // Initialize the registry client. - let client = RegistryClientBuilder::new(cache.clone()) - .connectivity(connectivity) - .build(); - // Read all requirements from the provided sources. let RequirementsSpecification { project, @@ -89,7 +84,7 @@ pub(crate) async fn pip_install( no_index, find_links, extras: used_extras, - } = specification(requirements, constraints, overrides, extras, &client).await?; + } = specification(requirements, constraints, overrides, extras, connectivity).await?; // Check that all provided extras are used if let ExtrasSpecification::Some(extras) = extras { @@ -180,9 +175,11 @@ pub(crate) async fn pip_install( let index_locations = index_locations.combine(index_url, extra_index_urls, find_links, no_index); - // Update the index URLs on the client, to take into account any index URLs added by the - // sources (e.g., `--index-url` in a `requirements.txt` file). - let client = client.with_index_url(index_locations.index_urls()); + // Initialize the registry client. + let client = RegistryClientBuilder::new(cache.clone()) + .connectivity(connectivity) + .index_urls(index_locations.index_urls()) + .build(); // Resolve the flat indexes from `--find-links`. let flat_index = { @@ -339,7 +336,7 @@ async fn specification( constraints: &[RequirementsSource], overrides: &[RequirementsSource], extras: &ExtrasSpecification<'_>, - client: &RegistryClient, + connectivity: Connectivity, ) -> Result { // If the user requests `extras` but does not provide a pyproject toml source if !matches!(extras, ExtrasSpecification::None) @@ -356,7 +353,7 @@ async fn specification( constraints, overrides, extras, - client, + connectivity, ) .await?; diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index a4d5dae3e7da..1715f08ad9aa 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -50,11 +50,6 @@ pub(crate) async fn pip_sync( ) -> Result { let start = std::time::Instant::now(); - // Initialize the registry client. - let client = RegistryClientBuilder::new(cache.clone()) - .connectivity(connectivity) - .build(); - // Read all requirements from the provided sources. let RequirementsSpecification { project: _project, @@ -67,7 +62,7 @@ pub(crate) async fn pip_sync( no_index, find_links, extras: _extras, - } = RequirementsSpecification::from_simple_sources(sources, &client).await?; + } = RequirementsSpecification::from_simple_sources(sources, connectivity).await?; let num_requirements = requirements.len() + editables.len(); if num_requirements == 0 { @@ -119,9 +114,11 @@ pub(crate) async fn pip_sync( let index_locations = index_locations.combine(index_url, extra_index_urls, find_links, no_index); - // Update the index URLs on the client, to take into account any index URLs added by the - // sources (e.g., `--index-url` in a `requirements.txt` file). - let client = client.with_index_url(index_locations.index_urls()); + // Initialize the registry client. + let client = RegistryClientBuilder::new(cache.clone()) + .connectivity(connectivity) + .index_urls(index_locations.index_urls()) + .build(); // Resolve the flat indexes from `--find-links`. let flat_index = { diff --git a/crates/uv/src/commands/pip_uninstall.rs b/crates/uv/src/commands/pip_uninstall.rs index a8c27d1bc333..51d3e2c56b00 100644 --- a/crates/uv/src/commands/pip_uninstall.rs +++ b/crates/uv/src/commands/pip_uninstall.rs @@ -7,7 +7,7 @@ use tracing::debug; use distribution_types::{InstalledMetadata, Name}; use platform_host::Platform; use uv_cache::Cache; -use uv_client::{Connectivity, RegistryClientBuilder}; +use uv_client::Connectivity; use uv_fs::Simplified; use uv_interpreter::PythonEnvironment; @@ -27,11 +27,6 @@ pub(crate) async fn pip_uninstall( ) -> Result { let start = std::time::Instant::now(); - // Initialize the registry client. - let client: uv_client::RegistryClient = RegistryClientBuilder::new(cache.clone()) - .connectivity(connectivity) - .build(); - // Read all requirements from the provided sources. let RequirementsSpecification { project: _project, @@ -44,7 +39,7 @@ pub(crate) async fn pip_uninstall( no_index: _no_index, find_links: _find_links, extras: _extras, - } = RequirementsSpecification::from_simple_sources(sources, &client).await?; + } = RequirementsSpecification::from_simple_sources(sources, connectivity).await?; // Detect the current Python interpreter. let platform = Platform::current()?; diff --git a/crates/uv/src/logging.rs b/crates/uv/src/logging.rs index 51ab11115c0c..67c491a4db8f 100644 --- a/crates/uv/src/logging.rs +++ b/crates/uv/src/logging.rs @@ -1,9 +1,21 @@ +use anstream::ColorChoice; +use std::fmt; +use std::str::FromStr; + +use anyhow::Context; +use chrono::Utc; +use owo_colors::OwoColorize; use tracing::level_filters::LevelFilter; +use tracing::{Event, Subscriber}; #[cfg(feature = "tracing-durations-export")] use tracing_durations_export::{ plot::PlotConfig, DurationsLayer, DurationsLayerBuilder, DurationsLayerDropGuard, }; +use tracing_subscriber::filter::Directive; +use tracing_subscriber::fmt::format::Writer; +use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields}; use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{EnvFilter, Layer, Registry}; use tracing_tree::time::Uptime; @@ -16,6 +28,83 @@ pub(crate) enum Level { Default, /// Show debug messages by default (overridable by `RUST_LOG`). Verbose, + /// Show messages in a hierarchical span tree. By default, debug messages are shown (overridable by `RUST_LOG`). + ExtraVerbose, +} + +struct UvFormat { + display_timestamp: bool, + display_level: bool, + show_spans: bool, +} + +/// See +impl FormatEvent for UvFormat +where + S: Subscriber + for<'a> LookupSpan<'a>, + N: for<'a> FormatFields<'a> + 'static, +{ + fn format_event( + &self, + ctx: &FmtContext<'_, S, N>, + mut writer: Writer<'_>, + event: &Event<'_>, + ) -> fmt::Result { + let meta = event.metadata(); + let ansi = writer.has_ansi_escapes(); + + if self.display_timestamp { + if ansi { + write!(writer, "{} ", Utc::now().dimmed())?; + } else { + write!(writer, "{} ", Utc::now())?; + } + } + + if self.display_level { + let level = meta.level(); + // Same colors as tracing + if ansi { + match *level { + tracing::Level::TRACE => write!(writer, "{} ", level.purple())?, + tracing::Level::DEBUG => write!(writer, "{} ", level.blue())?, + tracing::Level::INFO => write!(writer, "{} ", level.green())?, + tracing::Level::WARN => write!(writer, "{} ", level.yellow())?, + tracing::Level::ERROR => write!(writer, "{} ", level.red())?, + } + } else { + write!(writer, "{level} ")?; + } + } + + if self.show_spans { + let span = event.parent(); + let mut seen = false; + + let span = span + .and_then(|id| ctx.span(id)) + .or_else(|| ctx.lookup_current()); + + let scope = span.into_iter().flat_map(|span| span.scope().from_root()); + + for span in scope { + seen = true; + if ansi { + write!(writer, "{}:", span.metadata().name().bold())?; + } else { + write!(writer, "{}:", span.metadata().name())?; + } + } + + if seen { + writer.write_char(' ')?; + } + } + + ctx.field_format().format_fields(writer.by_ref(), event)?; + + writeln!(writer) + } } /// Configure `tracing` based on the given [`Level`], taking into account the `RUST_LOG` environment @@ -24,32 +113,52 @@ pub(crate) enum Level { /// The [`Level`] is used to dictate the default filters (which can be overridden by the `RUST_LOG` /// environment variable) along with the formatting of the output. For example, [`Level::Verbose`] /// includes targets and timestamps, along with all `uv=debug` messages by default. -pub(crate) fn setup_logging(level: Level, duration: impl Layer + Send + Sync) { - match level { +pub(crate) fn setup_logging( + level: Level, + duration: impl Layer + Send + Sync, +) -> anyhow::Result<()> { + let default_directive = match level { Level::Default => { // Show nothing, but allow `RUST_LOG` to override. - let filter = EnvFilter::builder() - .with_default_directive(LevelFilter::OFF.into()) - .from_env_lossy(); + LevelFilter::OFF.into() + } + Level::Verbose | Level::ExtraVerbose => { + // Show `DEBUG` messages from the CLI crate, but allow `RUST_LOG` to override. + Directive::from_str("uv=debug").unwrap() + } + }; + + let filter = EnvFilter::builder() + .with_default_directive(default_directive) + .from_env() + .context("Invalid RUST_LOG directives")?; + match level { + Level::Default | Level::Verbose => { // Regardless of the tracing level, show messages without any adornment. + let format = UvFormat { + display_timestamp: false, + display_level: true, + show_spans: false, + }; + let ansi = match anstream::Stderr::choice(&std::io::stderr()) { + ColorChoice::Always | ColorChoice::AlwaysAnsi => true, + ColorChoice::Never => false, + // We just asked anstream for a choice, that can't be auto + ColorChoice::Auto => unreachable!(), + }; tracing_subscriber::registry() .with(duration) .with(filter) .with( tracing_subscriber::fmt::layer() - .without_time() - .with_target(false) - .with_writer(std::io::sink), + .event_format(format) + .with_writer(std::io::stderr) + .with_ansi(ansi), ) .init(); } - Level::Verbose => { - // Show `DEBUG` messages from the CLI crate, but allow `RUST_LOG` to override. - let filter = EnvFilter::try_from_default_env() - .or_else(|_| EnvFilter::try_new("uv=debug")) - .unwrap(); - + Level::ExtraVerbose => { // Regardless of the tracing level, include the uptime and target for each message. tracing_subscriber::registry() .with(duration) @@ -63,6 +172,8 @@ pub(crate) fn setup_logging(level: Level, duration: impl Layer + Send .init(); } } + + Ok(()) } /// Setup the `TRACING_DURATIONS_FILE` environment variable to enable tracing durations. diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 4b4790452a9c..8271b1ff664c 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -68,8 +68,11 @@ struct Cli { quiet: bool, /// Use verbose output. - #[arg(global = true, long, short, conflicts_with = "quiet")] - verbose: bool, + /// + /// You can configure fine-grained logging using the `RUST_LOG` environment variable. + /// () + #[arg(global = true, action = clap::ArgAction::Count, long, short, conflicts_with = "quiet")] + verbose: u8, /// Disable colors; provided for compatibility with `pip`. #[arg(global = true, long, hide = true, conflicts_with = "color")] @@ -1255,18 +1258,18 @@ async fn run() -> Result { #[cfg(not(feature = "tracing-durations-export"))] let duration_layer = None::; logging::setup_logging( - if cli.verbose { - logging::Level::Verbose - } else { - logging::Level::Default + match cli.verbose { + 0 => logging::Level::Default, + 1 => logging::Level::Verbose, + 2.. => logging::Level::ExtraVerbose, }, duration_layer, - ); + )?; // Configure the `Printer`, which controls user-facing output in the CLI. let printer = if cli.quiet { printer::Printer::Quiet - } else if cli.verbose { + } else if cli.verbose > 0 { printer::Printer::Verbose } else { printer::Printer::Default diff --git a/crates/uv/src/requirements.rs b/crates/uv/src/requirements.rs index e5fe33ce99a0..123c9036be0b 100644 --- a/crates/uv/src/requirements.rs +++ b/crates/uv/src/requirements.rs @@ -12,7 +12,7 @@ use tracing::{instrument, Level}; use distribution_types::{FlatIndexLocation, IndexUrl}; use pep508_rs::Requirement; use requirements_txt::{EditableRequirement, FindLink, RequirementsTxt}; -use uv_client::RegistryClient; +use uv_client::Connectivity; use uv_fs::Simplified; use uv_normalize::{ExtraName, PackageName}; use uv_warnings::warn_user; @@ -142,7 +142,7 @@ impl RequirementsSpecification { pub(crate) async fn from_source( source: &RequirementsSource, extras: &ExtrasSpecification<'_>, - client: &RegistryClient, + connectivity: Connectivity, ) -> Result { Ok(match source { RequirementsSource::Package(name) => { @@ -179,7 +179,7 @@ impl RequirementsSpecification { } RequirementsSource::RequirementsTxt(path) => { let requirements_txt = - RequirementsTxt::parse(path, std::env::current_dir()?, Some(client)).await?; + RequirementsTxt::parse(path, std::env::current_dir()?, connectivity).await?; Self { project: None, requirements: requirements_txt @@ -281,7 +281,7 @@ impl RequirementsSpecification { constraints: &[RequirementsSource], overrides: &[RequirementsSource], extras: &ExtrasSpecification<'_>, - client: &RegistryClient, + connectivity: Connectivity, ) -> Result { let mut spec = Self::default(); @@ -289,7 +289,7 @@ impl RequirementsSpecification { // A `requirements.txt` can contain a `-c constraints.txt` directive within it, so reading // a requirements file can also add constraints. for source in requirements { - let source = Self::from_source(source, extras, client).await?; + let source = Self::from_source(source, extras, connectivity).await?; spec.requirements.extend(source.requirements); spec.constraints.extend(source.constraints); spec.overrides.extend(source.overrides); @@ -316,7 +316,7 @@ impl RequirementsSpecification { // Read all constraints, treating _everything_ as a constraint. for source in constraints { - let source = Self::from_source(source, extras, client).await?; + let source = Self::from_source(source, extras, connectivity).await?; spec.constraints.extend(source.requirements); spec.constraints.extend(source.constraints); spec.constraints.extend(source.overrides); @@ -336,7 +336,7 @@ impl RequirementsSpecification { // Read all overrides, treating both requirements _and_ constraints as overrides. for source in overrides { - let source = Self::from_source(source, extras, client).await?; + let source = Self::from_source(source, extras, connectivity).await?; spec.overrides.extend(source.requirements); spec.overrides.extend(source.constraints); spec.overrides.extend(source.overrides); @@ -360,9 +360,16 @@ impl RequirementsSpecification { /// Read the requirements from a set of sources. pub(crate) async fn from_simple_sources( requirements: &[RequirementsSource], - client: &RegistryClient, + connectivity: Connectivity, ) -> Result { - Self::from_sources(requirements, &[], &[], &ExtrasSpecification::None, client).await + Self::from_sources( + requirements, + &[], + &[], + &ExtrasSpecification::None, + connectivity, + ) + .await } } @@ -449,7 +456,8 @@ pub(crate) async fn read_lockfile( // Parse the requirements from the lockfile. let requirements_txt = - RequirementsTxt::parse(output_file, std::env::current_dir()?, None).await?; + RequirementsTxt::parse(output_file, std::env::current_dir()?, Connectivity::Offline) + .await?; let requirements = requirements_txt .requirements .into_iter() diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 2205ef3c4570..7a7bb8cbf3e5 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -1499,8 +1499,7 @@ fn install_constraints_respects_offline_mode() { ----- stdout ----- ----- stderr ----- - error: Error while accessing remote requirements file http://example.com/requirements.txt: Middleware error: Network connectivity is disabled, but the requested data wasn't found in the cache for: `http://example.com/requirements.txt` - Caused by: Network connectivity is disabled, but the requested data wasn't found in the cache for: `http://example.com/requirements.txt` + error: Network connectivity is disabled, but a remote requirements file was requested: http://example.com/requirements.txt "### ); } diff --git a/crates/uv/tests/pip_show.rs b/crates/uv/tests/pip_show.rs index fb6765b2d6bd..78d9de9398de 100644 --- a/crates/uv/tests/pip_show.rs +++ b/crates/uv/tests/pip_show.rs @@ -116,14 +116,13 @@ fn show_requires_multiple() -> Result<()> { } #[test] -#[cfg(not(windows))] fn show_python_version_marker() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.touch()?; - // requires different numpy versions depending on the python version used. - requirements_txt.write_str("pandas==2.1.3")?; + // ./click-8.1.7.dist-info/METADATA:Requires-Dist: importlib-metadata ; python_version < "3.8" + requirements_txt.write_str("click==8.1.7")?; uv_snapshot!(install_command(&context) .arg("-r") @@ -134,29 +133,27 @@ fn show_python_version_marker() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 6 packages in [TIME] - Downloaded 6 packages in [TIME] - Installed 6 packages in [TIME] - + numpy==1.26.2 - + pandas==2.1.3 - + python-dateutil==2.8.2 - + pytz==2023.3.post1 - + six==1.16.0 - + tzdata==2023.3 + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + click==8.1.7 "### ); - context.assert_command("import pandas").success(); - let filters = [( + context.assert_command("import click").success(); + let mut filters = [( r"Location:.*site-packages", "Location: [WORKSPACE_DIR]/site-packages", )] .to_vec(); + if cfg!(windows) { + filters.push(("Requires: colorama", "Requires: ")); + } uv_snapshot!(filters, Command::new(get_bin()) .arg("pip") .arg("show") - .arg("pandas") + .arg("click") .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) @@ -164,10 +161,10 @@ fn show_python_version_marker() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - Name: pandas - Version: 2.1.3 + Name: click + Version: 8.1.7 Location: [WORKSPACE_DIR]/site-packages - Requires: numpy, python-dateutil, pytz, tzdata + Requires: ----- stderr ----- "### From 915be6708beee2de6347004c528b4aa59d0294f9 Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Mon, 11 Mar 2024 22:31:53 -0400 Subject: [PATCH 5/6] address comment --- crates/uv/src/commands/pip_show.rs | 3 +++ crates/uv/tests/pip_show.rs | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/uv/src/commands/pip_show.rs b/crates/uv/src/commands/pip_show.rs index cc5baddf1a57..7ebfe7f84588 100644 --- a/crates/uv/src/commands/pip_show.rs +++ b/crates/uv/src/commands/pip_show.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; use std::fmt::Write; use anyhow::Result; @@ -127,6 +128,8 @@ pub(crate) fn pip_show( .into_iter() .filter(|req| req.evaluate_markers(markers, &[])) .map(|req| req.name.to_string()) + .collect::>() + .into_iter() .collect::>(); requires.sort(); writeln!(printer.stdout(), "Requires: {}", requires.join(", "))?; diff --git a/crates/uv/tests/pip_show.rs b/crates/uv/tests/pip_show.rs index 78d9de9398de..03d1d00a68d6 100644 --- a/crates/uv/tests/pip_show.rs +++ b/crates/uv/tests/pip_show.rs @@ -115,13 +115,14 @@ fn show_requires_multiple() -> Result<()> { Ok(()) } +// Asserts python version marker in the metadata is correctly evaluated. +// click 8.1.7 requires importlib-metadata but only when python_version < "3.8" #[test] fn show_python_version_marker() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.touch()?; - // ./click-8.1.7.dist-info/METADATA:Requires-Dist: importlib-metadata ; python_version < "3.8" requirements_txt.write_str("click==8.1.7")?; uv_snapshot!(install_command(&context) From bc98a1016a7e0c3624ce809a8d7a4b1fb09c1ca4 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 12 Mar 2024 00:22:39 -0400 Subject: [PATCH 6/6] Avoid vec --- crates/uv/src/commands/pip_show.rs | 31 ++++++++++++++++++------------ crates/uv/tests/pip_show.rs | 28 +++++++++++++-------------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/crates/uv/src/commands/pip_show.rs b/crates/uv/src/commands/pip_show.rs index 7ebfe7f84588..c134ff4fa651 100644 --- a/crates/uv/src/commands/pip_show.rs +++ b/crates/uv/src/commands/pip_show.rs @@ -2,6 +2,7 @@ use std::collections::BTreeSet; use std::fmt::Write; use anyhow::Result; +use itertools::Itertools; use owo_colors::OwoColorize; use tracing::debug; @@ -121,18 +122,24 @@ pub(crate) fn pip_show( .simplified_display() )?; - let mut requires = distribution - .metadata() - .unwrap() - .requires_dist - .into_iter() - .filter(|req| req.evaluate_markers(markers, &[])) - .map(|req| req.name.to_string()) - .collect::>() - .into_iter() - .collect::>(); - requires.sort(); - writeln!(printer.stdout(), "Requires: {}", requires.join(", "))?; + // If available, print the requirements. + if let Ok(metadata) = distribution.metadata() { + let requires_dist = metadata + .requires_dist + .into_iter() + .filter(|req| req.evaluate_markers(markers, &[])) + .map(|req| req.name) + .collect::>(); + if requires_dist.is_empty() { + writeln!(printer.stdout(), "Requires:")?; + } else { + writeln!( + printer.stdout(), + "Requires: {}", + requires_dist.into_iter().join(", ") + )?; + } + } } // Validate that the environment is consistent. diff --git a/crates/uv/tests/pip_show.rs b/crates/uv/tests/pip_show.rs index 03d1d00a68d6..fc591e184a77 100644 --- a/crates/uv/tests/pip_show.rs +++ b/crates/uv/tests/pip_show.rs @@ -115,8 +115,8 @@ fn show_requires_multiple() -> Result<()> { Ok(()) } -// Asserts python version marker in the metadata is correctly evaluated. -// click 8.1.7 requires importlib-metadata but only when python_version < "3.8" +/// Asserts that the Python version marker in the metadata is correctly evaluated. +/// `click` v8.1.7 requires `importlib-metadata`, but only when `python_version < "3.8"`. #[test] fn show_python_version_marker() -> Result<()> { let context = TestContext::new("3.12"); @@ -142,13 +142,13 @@ fn show_python_version_marker() -> Result<()> { ); context.assert_command("import click").success(); - let mut filters = [( + + let mut filters = vec![( r"Location:.*site-packages", "Location: [WORKSPACE_DIR]/site-packages", - )] - .to_vec(); + )]; if cfg!(windows) { - filters.push(("Requires: colorama", "Requires: ")); + filters.push(("Requires: colorama", "Requires:")); } uv_snapshot!(filters, Command::new(get_bin()) @@ -165,7 +165,7 @@ fn show_python_version_marker() -> Result<()> { Name: click Version: 8.1.7 Location: [WORKSPACE_DIR]/site-packages - Requires: + Requires: ----- stderr ----- "### @@ -199,11 +199,11 @@ fn show_found_single_package() -> Result<()> { ); context.assert_command("import markupsafe").success(); - let filters = [( + + let filters = vec![( r"Location:.*site-packages", "Location: [WORKSPACE_DIR]/site-packages", - )] - .to_vec(); + )]; uv_snapshot!(filters, Command::new(get_bin()) .arg("pip") @@ -219,7 +219,7 @@ fn show_found_single_package() -> Result<()> { Name: markupsafe Version: 2.1.3 Location: [WORKSPACE_DIR]/site-packages - Requires: + Requires: ----- stderr ----- "### @@ -281,12 +281,12 @@ fn show_found_multiple_packages() -> Result<()> { Name: markupsafe Version: 2.1.3 Location: [WORKSPACE_DIR]/site-packages - Requires: + Requires: --- Name: pip Version: 21.3.1 Location: [WORKSPACE_DIR]/site-packages - Requires: + Requires: ----- stderr ----- "### @@ -348,7 +348,7 @@ fn show_found_one_out_of_two() -> Result<()> { Name: markupsafe Version: 2.1.3 Location: [WORKSPACE_DIR]/site-packages - Requires: + Requires: ----- stderr ----- warning: Package(s) not found for: flask