diff --git a/crates/distribution-types/src/cached.rs b/crates/distribution-types/src/cached.rs index 958ad8fa2..1cea68bf0 100644 --- a/crates/distribution-types/src/cached.rs +++ b/crates/distribution-types/src/cached.rs @@ -49,7 +49,7 @@ impl Metadata for CachedDirectUrlDist { } fn version_or_url(&self) -> VersionOrUrl { - VersionOrUrl::Url(&self.url) + VersionOrUrl::VersionedUrl(self.url.raw(), &self.filename.version) } } diff --git a/crates/distribution-types/src/installed.rs b/crates/distribution-types/src/installed.rs index 8a2a7cc50..ce10d01b1 100644 --- a/crates/distribution-types/src/installed.rs +++ b/crates/distribution-types/src/installed.rs @@ -7,7 +7,6 @@ use url::Url; use pep440_rs::Version; use puffin_normalize::PackageName; -use pypi_types::{DirectUrl, Metadata21}; use crate::{Metadata, VersionOrUrl}; @@ -31,7 +30,8 @@ pub struct InstalledRegistryDist { pub struct InstalledDirectUrlDist { pub name: PackageName, pub version: Version, - pub url: DirectUrl, + pub url: Url, + pub editable: bool, pub path: PathBuf, } @@ -51,8 +51,7 @@ impl Metadata for InstalledDirectUrlDist { } fn version_or_url(&self) -> VersionOrUrl { - // TODO(charlie): Convert a `DirectUrl` to `Url`. - VersionOrUrl::Version(&self.version) + VersionOrUrl::VersionedUrl(&self.url, &self.version) } } @@ -94,7 +93,8 @@ impl InstalledDist { Ok(Some(Self::Url(InstalledDirectUrlDist { name, version, - url: direct_url, + editable: matches!(&direct_url, pypi_types::DirectUrl::LocalDirectory { dir_info, .. } if dir_info.editable == Some(true)), + url: Url::from(direct_url), path: path.to_path_buf(), }))) } else { @@ -125,31 +125,28 @@ impl InstalledDist { } /// Read the `direct_url.json` file from a `.dist-info` directory. - fn direct_url(path: &Path) -> Result> { + fn direct_url(path: &Path) -> Result> { let path = path.join("direct_url.json"); let Ok(file) = fs_err::File::open(path) else { return Ok(None); }; - let direct_url = serde_json::from_reader::(file)?; + let direct_url = serde_json::from_reader::(file)?; Ok(Some(direct_url)) } /// Read the `METADATA` file from a `.dist-info` directory. - pub fn metadata(&self) -> Result { + pub fn metadata(&self) -> Result { let path = self.path().join("METADATA"); let contents = fs::read(&path)?; - Metadata21::parse(&contents) + pypi_types::Metadata21::parse(&contents) .with_context(|| format!("Failed to parse METADATA file at: {}", path.display())) } /// Return the [`Url`] of the distribution, if it is editable. - pub fn editable(&self) -> Option<&Url> { + pub fn as_editable(&self) -> Option<&Url> { match self { - Self::Url(InstalledDirectUrlDist { - url: DirectUrl::LocalDirectory { url, dir_info }, - .. - }) if dir_info.editable == Some(true) => Some(url), - _ => None, + Self::Registry(_) => None, + Self::Url(dist) => dist.editable.then_some(&dist.url), } } } diff --git a/crates/distribution-types/src/lib.rs b/crates/distribution-types/src/lib.rs index dd21ec32c..61bbf2815 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/distribution-types/src/lib.rs @@ -75,6 +75,13 @@ pub enum VersionOrUrl<'a> { Version(&'a Version), /// A URL, used to identify a distribution at an arbitrary location. Url(&'a VerbatimUrl), + /// A URL, used to identify a distribution at an arbitrary location, along with the version + /// specifier to which it resolved. This is typically derived from a distribution that's already + /// been built and perhaps even installed on-disk, as the version specifier is not available + /// from the URL itself. As such, the URL is not guaranteed to be verbatim, as it could've been + /// serialized to disk and deserialized back from the virtual environment's `direct_url.json`. + /// TODO(charlie): Separate into a distinct enum to avoid this confusion. + VersionedUrl(&'a Url, &'a Version), } impl Verbatim for VersionOrUrl<'_> { @@ -82,6 +89,7 @@ impl Verbatim for VersionOrUrl<'_> { match self { VersionOrUrl::Version(version) => Cow::Owned(format!("=={version}")), VersionOrUrl::Url(url) => Cow::Owned(format!(" @ {}", url.verbatim())), + VersionOrUrl::VersionedUrl(url, ..) => Cow::Owned(format!(" @ {url}")), } } } @@ -91,6 +99,7 @@ impl std::fmt::Display for VersionOrUrl<'_> { match self { VersionOrUrl::Version(version) => write!(f, "=={version}"), VersionOrUrl::Url(url) => write!(f, " @ {url}"), + VersionOrUrl::VersionedUrl(url, version) => write!(f, "=={version} (from {url})"), } } } diff --git a/crates/distribution-types/src/traits.rs b/crates/distribution-types/src/traits.rs index 324fc583a..342d3c105 100644 --- a/crates/distribution-types/src/traits.rs +++ b/crates/distribution-types/src/traits.rs @@ -35,6 +35,7 @@ pub trait Metadata { format!("{}-{}", self.name().as_dist_info_name(), version) } VersionOrUrl::Url(url) => puffin_cache::digest(&CanonicalUrl::new(url)), + VersionOrUrl::VersionedUrl(url, ..) => puffin_cache::digest(&CanonicalUrl::new(url)), }) } } diff --git a/crates/puffin-cli/src/commands/reporters.rs b/crates/puffin-cli/src/commands/reporters.rs index bc66f763b..0be37a48b 100644 --- a/crates/puffin-cli/src/commands/reporters.rs +++ b/crates/puffin-cli/src/commands/reporters.rs @@ -234,6 +234,9 @@ impl puffin_resolver::ResolverReporter for ResolverReporter { VersionOrUrl::Url(url) => { self.progress.set_message(format!("{name} @ {url}")); } + VersionOrUrl::VersionedUrl(url, ..) => { + self.progress.set_message(format!("{name} @ {url}")); + } } } diff --git a/crates/puffin-cli/tests/pip_install.rs b/crates/puffin-cli/tests/pip_install.rs index 8c71c49a6..295dbb383 100644 --- a/crates/puffin-cli/tests/pip_install.rs +++ b/crates/puffin-cli/tests/pip_install.rs @@ -432,12 +432,11 @@ fn install_editable() -> Result<()> { let cache_dir = assert_fs::TempDir::new()?; let venv = create_venv_py312(&temp_dir, &cache_dir); - let current_dir = std::env::current_dir()? - .join("..") - .join("..") - .canonicalize()?; + let current_dir = std::env::current_dir()?; + let workspace_dir = current_dir.join("..").join("..").canonicalize()?; + let mut filters = INSTA_FILTERS.to_vec(); - filters.push((current_dir.to_str().unwrap(), "[CURRENT_DIR]")); + filters.push((workspace_dir.to_str().unwrap(), "[WORKSPACE_DIR]")); // Install the editable package. insta::with_settings!({ @@ -461,7 +460,7 @@ fn install_editable() -> Result<()> { Downloaded 1 package in [TIME] Installed 2 packages in [TIME] + numpy==1.26.2 - + poetry-editable @ file://[CURRENT_DIR]/scripts/editable-installs/poetry_editable/ + + poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable/) "###); }); @@ -520,8 +519,8 @@ fn install_editable() -> Result<()> { + packaging==23.2 + pathspec==0.12.1 + platformdirs==4.1.0 - - poetry-editable==0.1.0 - + poetry-editable @ file://[CURRENT_DIR]/scripts/editable-installs/poetry_editable/ + - poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable/) + + poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable/) + yarl==1.9.4 "###); }); @@ -549,9 +548,9 @@ fn install_editable() -> Result<()> { Built 2 editables in [TIME] Resolved 16 packages in [TIME] Installed 2 packages in [TIME] - + maturin-editable @ file://[CURRENT_DIR]/scripts/editable-installs/maturin_editable/ - - poetry-editable==0.1.0 - + poetry-editable @ file://[CURRENT_DIR]/scripts/editable-installs/poetry_editable/ + + maturin-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/maturin_editable/) + - poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable/) + + poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable/) "###); }); @@ -564,12 +563,11 @@ fn install_editable_and_registry() -> Result<()> { let cache_dir = assert_fs::TempDir::new()?; let venv = create_venv_py312(&temp_dir, &cache_dir); - let current_dir = std::env::current_dir()? - .join("..") - .join("..") - .canonicalize()?; + let current_dir = std::env::current_dir()?; + let workspace_dir = current_dir.join("..").join("..").canonicalize()?; + let mut filters = INSTA_FILTERS.to_vec(); - filters.push((current_dir.to_str().unwrap(), "[CURRENT_DIR]")); + filters.push((workspace_dir.to_str().unwrap(), "[WORKSPACE_DIR]")); // Install the registry-based version of Black. insta::with_settings!({ @@ -627,7 +625,7 @@ fn install_editable_and_registry() -> Result<()> { Resolved 1 package in [TIME] Installed 1 package in [TIME] - black==23.12.0 - + black @ file://[CURRENT_DIR]/scripts/editable-installs/black_editable/ + + black==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/black_editable/) "###); }); @@ -671,7 +669,7 @@ fn install_editable_and_registry() -> Result<()> { Resolved 6 packages in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - - black==0.1.0 + - black==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/black_editable/) + black==23.10.0 "###); }); diff --git a/crates/puffin-cli/tests/pip_sync.rs b/crates/puffin-cli/tests/pip_sync.rs index ea023c5ea..0897b6716 100644 --- a/crates/puffin-cli/tests/pip_sync.rs +++ b/crates/puffin-cli/tests/pip_sync.rs @@ -542,7 +542,7 @@ fn install_url() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + werkzeug @ https://files.pythonhosted.org/packages/ff/1d/960bb4017c68674a1cb099534840f18d3def3ce44aed12b5ed8b78e0153e/Werkzeug-2.0.0-py3-none-any.whl + + werkzeug==2.0.0 (from https://files.pythonhosted.org/packages/ff/1d/960bb4017c68674a1cb099534840f18d3def3ce44aed12b5ed8b78e0153e/Werkzeug-2.0.0-py3-none-any.whl) "###); }); @@ -583,7 +583,7 @@ fn install_git_commit() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + werkzeug @ git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74 + + werkzeug==2.0.0 (from git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74) "###); }); @@ -624,7 +624,7 @@ fn install_git_tag() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + werkzeug @ git+https://github.com/pallets/werkzeug.git@2.0.0 + + werkzeug==2.0.0 (from git+https://github.com/pallets/werkzeug.git@2.0.0) "###); }); @@ -665,8 +665,8 @@ fn install_git_subdirectories() -> Result<()> { Resolved 2 packages in [TIME] Downloaded 2 packages in [TIME] Installed 2 packages in [TIME] - + example-pkg-a @ git+https://github.com/pypa/sample-namespace-packages.git@df7530eeb8fa0cb7dbb8ecb28363e8e36bfa2f45#subdirectory=pkg_resources/pkg_a - + example-pkg-b @ git+https://github.com/pypa/sample-namespace-packages.git@df7530eeb8fa0cb7dbb8ecb28363e8e36bfa2f45#subdirectory=pkg_resources/pkg_b + + example-pkg-a==1 (from git+https://github.com/pypa/sample-namespace-packages.git@df7530eeb8fa0cb7dbb8ecb28363e8e36bfa2f45#subdirectory=pkg_resources/pkg_a) + + example-pkg-b==1 (from git+https://github.com/pypa/sample-namespace-packages.git@df7530eeb8fa0cb7dbb8ecb28363e8e36bfa2f45#subdirectory=pkg_resources/pkg_b) "###); }); @@ -748,7 +748,7 @@ fn install_sdist_url() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + werkzeug @ https://files.pythonhosted.org/packages/63/69/5702e5eb897d1a144001e21d676676bcb87b88c0862f947509ea95ea54fc/Werkzeug-0.9.6.tar.gz + + werkzeug==0.9.6 (from https://files.pythonhosted.org/packages/63/69/5702e5eb897d1a144001e21d676676bcb87b88c0862f947509ea95ea54fc/Werkzeug-0.9.6.tar.gz) "###); }); @@ -905,7 +905,7 @@ fn install_version_then_install_url() -> Result<()> { Uninstalled 1 package in [TIME] Installed 1 package in [TIME] - werkzeug==2.0.0 - + werkzeug @ https://files.pythonhosted.org/packages/ff/1d/960bb4017c68674a1cb099534840f18d3def3ce44aed12b5ed8b78e0153e/Werkzeug-2.0.0-py3-none-any.whl + + werkzeug==2.0.0 (from https://files.pythonhosted.org/packages/ff/1d/960bb4017c68674a1cb099534840f18d3def3ce44aed12b5ed8b78e0153e/Werkzeug-2.0.0-py3-none-any.whl) "###); }); @@ -1042,7 +1042,7 @@ fn install_local_wheel() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + tomli @ file://[TEMP_DIR]/tomli-3.0.1-py3-none-any.whl + + tomli==3.0.1 (from file://[TEMP_DIR]/tomli-3.0.1-py3-none-any.whl) "###); }); @@ -1089,7 +1089,7 @@ fn install_local_source_distribution() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + wheel @ file://[TEMP_DIR]/wheel-0.42.0.tar.gz + + wheel==0.42.0 (from file://[TEMP_DIR]/wheel-0.42.0.tar.gz) "###); }); @@ -1135,7 +1135,7 @@ fn install_ujson() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + ujson @ https://files.pythonhosted.org/packages/43/1a/b0a027144aa5c8f4ea654f4afdd634578b450807bb70b9f8bad00d6f6d3c/ujson-5.7.0.tar.gz + + ujson==5.7.0 (from https://files.pythonhosted.org/packages/43/1a/b0a027144aa5c8f4ea654f4afdd634578b450807bb70b9f8bad00d6f6d3c/ujson-5.7.0.tar.gz) "###); }); @@ -1181,7 +1181,7 @@ fn install_dtls_socket() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + dtlssocket @ https://files.pythonhosted.org/packages/58/42/0a0442118096eb9fbc9dc70b45aee2957f7546b80545e2a05bd839380519/DTLSSocket-0.1.16.tar.gz + + dtlssocket==0.1.16 (from https://files.pythonhosted.org/packages/58/42/0a0442118096eb9fbc9dc70b45aee2957f7546b80545e2a05bd839380519/DTLSSocket-0.1.16.tar.gz) warning: The package `dtlssocket` requires `cython <3`, but it's not installed. "###); }); @@ -1220,7 +1220,7 @@ fn install_url_source_dist_cached() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + tqdm @ https://files.pythonhosted.org/packages/62/06/d5604a70d160f6a6ca5fd2ba25597c24abd5c5ca5f437263d177ac242308/tqdm-4.66.1.tar.gz + + tqdm==4.66.1 (from https://files.pythonhosted.org/packages/62/06/d5604a70d160f6a6ca5fd2ba25597c24abd5c5ca5f437263d177ac242308/tqdm-4.66.1.tar.gz) "###); }); @@ -1246,7 +1246,7 @@ fn install_url_source_dist_cached() -> Result<()> { ----- stderr ----- Installed 1 package in [TIME] - + tqdm @ https://files.pythonhosted.org/packages/62/06/d5604a70d160f6a6ca5fd2ba25597c24abd5c5ca5f437263d177ac242308/tqdm-4.66.1.tar.gz + + tqdm==4.66.1 (from https://files.pythonhosted.org/packages/62/06/d5604a70d160f6a6ca5fd2ba25597c24abd5c5ca5f437263d177ac242308/tqdm-4.66.1.tar.gz) "###); }); @@ -1293,7 +1293,7 @@ fn install_url_source_dist_cached() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + tqdm @ https://files.pythonhosted.org/packages/62/06/d5604a70d160f6a6ca5fd2ba25597c24abd5c5ca5f437263d177ac242308/tqdm-4.66.1.tar.gz + + tqdm==4.66.1 (from https://files.pythonhosted.org/packages/62/06/d5604a70d160f6a6ca5fd2ba25597c24abd5c5ca5f437263d177ac242308/tqdm-4.66.1.tar.gz) "###); }); @@ -1332,7 +1332,7 @@ fn install_git_source_dist_cached() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + werkzeug @ git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74 + + werkzeug==2.0.0 (from git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74) "###); }); @@ -1358,7 +1358,7 @@ fn install_git_source_dist_cached() -> Result<()> { ----- stderr ----- Installed 1 package in [TIME] - + werkzeug @ git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74 + + werkzeug==2.0.0 (from git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74) "###); }); @@ -1405,7 +1405,7 @@ fn install_git_source_dist_cached() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + werkzeug @ git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74 + + werkzeug==2.0.0 (from git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74) "###); }); @@ -1563,7 +1563,7 @@ fn install_path_source_dist_cached() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + wheel @ file://[TEMP_DIR]/wheel-0.42.0.tar.gz + + wheel==0.42.0 (from file://[TEMP_DIR]/wheel-0.42.0.tar.gz) "###); }); @@ -1589,7 +1589,7 @@ fn install_path_source_dist_cached() -> Result<()> { ----- stderr ----- Installed 1 package in [TIME] - + wheel @ file://[TEMP_DIR]/wheel-0.42.0.tar.gz + + wheel==0.42.0 (from file://[TEMP_DIR]/wheel-0.42.0.tar.gz) "###); }); @@ -1636,7 +1636,7 @@ fn install_path_source_dist_cached() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + wheel @ file://[TEMP_DIR]/wheel-0.42.0.tar.gz + + wheel==0.42.0 (from file://[TEMP_DIR]/wheel-0.42.0.tar.gz) "###); }); @@ -1683,7 +1683,7 @@ fn install_path_built_dist_cached() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + tomli @ file://[TEMP_DIR]/tomli-3.0.1-py3-none-any.whl + + tomli==3.0.1 (from file://[TEMP_DIR]/tomli-3.0.1-py3-none-any.whl) "###); }); @@ -1709,7 +1709,7 @@ fn install_path_built_dist_cached() -> Result<()> { ----- stderr ----- Installed 1 package in [TIME] - + tomli @ file://[TEMP_DIR]/tomli-3.0.1-py3-none-any.whl + + tomli==3.0.1 (from file://[TEMP_DIR]/tomli-3.0.1-py3-none-any.whl) "###); }); @@ -1756,7 +1756,7 @@ fn install_path_built_dist_cached() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + tomli @ file://[TEMP_DIR]/tomli-3.0.1-py3-none-any.whl + + tomli==3.0.1 (from file://[TEMP_DIR]/tomli-3.0.1-py3-none-any.whl) "###); }); @@ -1794,7 +1794,7 @@ fn install_url_built_dist_cached() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + tqdm @ https://files.pythonhosted.org/packages/00/e5/f12a80907d0884e6dff9c16d0c0114d81b8cd07dc3ae54c5e962cc83037e/tqdm-4.66.1-py3-none-any.whl + + tqdm==4.66.1 (from https://files.pythonhosted.org/packages/00/e5/f12a80907d0884e6dff9c16d0c0114d81b8cd07dc3ae54c5e962cc83037e/tqdm-4.66.1-py3-none-any.whl) "###); }); @@ -1820,7 +1820,7 @@ fn install_url_built_dist_cached() -> Result<()> { ----- stderr ----- Installed 1 package in [TIME] - + tqdm @ https://files.pythonhosted.org/packages/00/e5/f12a80907d0884e6dff9c16d0c0114d81b8cd07dc3ae54c5e962cc83037e/tqdm-4.66.1-py3-none-any.whl + + tqdm==4.66.1 (from https://files.pythonhosted.org/packages/00/e5/f12a80907d0884e6dff9c16d0c0114d81b8cd07dc3ae54c5e962cc83037e/tqdm-4.66.1-py3-none-any.whl) "###); }); @@ -1867,7 +1867,7 @@ fn install_url_built_dist_cached() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + tqdm @ https://files.pythonhosted.org/packages/00/e5/f12a80907d0884e6dff9c16d0c0114d81b8cd07dc3ae54c5e962cc83037e/tqdm-4.66.1-py3-none-any.whl + + tqdm==4.66.1 (from https://files.pythonhosted.org/packages/00/e5/f12a80907d0884e6dff9c16d0c0114d81b8cd07dc3ae54c5e962cc83037e/tqdm-4.66.1-py3-none-any.whl) "###); }); @@ -2087,6 +2087,74 @@ fn reinstall_package() -> Result<()> { Ok(()) } +/// Verify that we can force reinstall of Git dependencies. +#[test] +#[cfg(feature = "git")] +fn reinstall_git() -> Result<()> { + let temp_dir = assert_fs::TempDir::new()?; + let cache_dir = assert_fs::TempDir::new()?; + let venv = create_venv_py312(&temp_dir, &cache_dir); + + let requirements_txt = temp_dir.child("requirements.txt"); + requirements_txt.touch()?; + requirements_txt.write_str("werkzeug @ git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74")?; + + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec() + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .arg("pip-sync") + .arg("requirements.txt") + .arg("--cache-dir") + .arg(cache_dir.path()) + .env("VIRTUAL_ENV", venv.as_os_str()) + .current_dir(&temp_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + werkzeug==2.0.0 (from git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74) + "###); + }); + + check_command(&venv, "import werkzeug", &temp_dir); + + // Re-run the installation with `--reinstall`. + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec() + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .arg("pip-sync") + .arg("requirements.txt") + .arg("--reinstall-package") + .arg("WerkZeug") + .arg("--cache-dir") + .arg(cache_dir.path()) + .env("VIRTUAL_ENV", venv.as_os_str()) + .current_dir(&temp_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - werkzeug==2.0.0 (from git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74) + + werkzeug==2.0.0 (from git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74) + "###); + }); + + check_command(&venv, "import werkzeug", &temp_dir); + + Ok(()) +} + #[test] fn sync_editable() -> Result<()> { let temp_dir = assert_fs::TempDir::new()?; @@ -2116,7 +2184,7 @@ fn sync_editable() -> Result<()> { r"file://.*/../../scripts/editable-installs/poetry_editable", "file://[TEMP_DIR]/../../scripts/editable-installs/poetry_editable", ), - (workspace_dir.to_str().unwrap(), "[CURRENT_DIR]"), + (workspace_dir.to_str().unwrap(), "[WORKSPACE_DIR]"), ]) .copied() .collect::>(); @@ -2142,9 +2210,9 @@ fn sync_editable() -> Result<()> { Downloaded 2 packages in [TIME] Installed 4 packages in [TIME] + boltons==23.1.1 - + maturin-editable @ file://[CURRENT_DIR]/scripts/editable-installs/maturin_editable/ + + maturin-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/maturin_editable/) + numpy==1.26.2 - + poetry-editable @ file://[CURRENT_DIR]/scripts/editable-installs/poetry_editable + + poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable) "###); }); @@ -2169,8 +2237,8 @@ fn sync_editable() -> Result<()> { Built 1 editable in [TIME] Uninstalled 1 package in [TIME] Installed 1 package in [TIME] - - poetry-editable==0.1.0 - + poetry-editable @ file://[CURRENT_DIR]/scripts/editable-installs/poetry_editable + - poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable) + + poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable) "###); }); @@ -2253,7 +2321,7 @@ fn sync_editable_and_registry() -> Result<()> { .iter() .chain(&[ (filter_path.as_str(), "requirements.txt"), - (workspace_dir.to_str().unwrap(), "[CURRENT_DIR]"), + (workspace_dir.to_str().unwrap(), "[WORKSPACE_DIR]"), ]) .copied() .collect::>(); @@ -2297,7 +2365,7 @@ fn sync_editable_and_registry() -> Result<()> { .iter() .chain(&[ (filter_path.as_str(), "requirements.txt"), - (workspace_dir.to_str().unwrap(), "[CURRENT_DIR]"), + (workspace_dir.to_str().unwrap(), "[WORKSPACE_DIR]"), ]) .copied() .collect::>(); @@ -2320,7 +2388,7 @@ fn sync_editable_and_registry() -> Result<()> { Uninstalled 1 package in [TIME] Installed 1 package in [TIME] - black==24.1a1 - + black @ file://[CURRENT_DIR]/scripts/editable-installs/black_editable/ + + black==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/black_editable/) "###); }); @@ -2337,7 +2405,7 @@ fn sync_editable_and_registry() -> Result<()> { .iter() .chain(&[ (filter_path.as_str(), "requirements.txt"), - (workspace_dir.to_str().unwrap(), "[CURRENT_DIR]"), + (workspace_dir.to_str().unwrap(), "[WORKSPACE_DIR]"), ]) .copied() .collect::>(); @@ -2372,7 +2440,7 @@ fn sync_editable_and_registry() -> Result<()> { .iter() .chain(&[ (filter_path.as_str(), "requirements.txt"), - (workspace_dir.to_str().unwrap(), "[CURRENT_DIR]"), + (workspace_dir.to_str().unwrap(), "[WORKSPACE_DIR]"), ]) .copied() .collect::>(); @@ -2395,7 +2463,7 @@ fn sync_editable_and_registry() -> Result<()> { Downloaded 1 package in [TIME] Uninstalled 1 package in [TIME] Installed 1 package in [TIME] - - black==0.1.0 + - black==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/black_editable/) + black==23.10.0 warning: The package `black` requires `click >=8.0.0`, but it's not installed. warning: The package `black` requires `mypy-extensions >=0.4.3`, but it's not installed. diff --git a/crates/puffin-cli/tests/pip_uninstall.rs b/crates/puffin-cli/tests/pip_uninstall.rs index d40abdf2b..ab9054862 100644 --- a/crates/puffin-cli/tests/pip_uninstall.rs +++ b/crates/puffin-cli/tests/pip_uninstall.rs @@ -289,6 +289,12 @@ fn uninstall_editable_by_name() -> Result<()> { let cache_dir = assert_fs::TempDir::new()?; let venv = temp_dir.child(".venv"); + let current_dir = std::env::current_dir()?; + let workspace_dir = current_dir.join("..").join("..").canonicalize()?; + + let mut filters = INSTA_FILTERS.to_vec(); + filters.push((workspace_dir.to_str().unwrap(), "[WORKSPACE_DIR]")); + Command::new(get_cargo_bin(BIN_NAME)) .arg("venv") .arg(venv.as_os_str()) @@ -320,7 +326,7 @@ fn uninstall_editable_by_name() -> Result<()> { // Uninstall the editable by name. insta::with_settings!({ - filters => INSTA_FILTERS.to_vec() + filters => filters.clone() }, { assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) .arg("pip-uninstall") @@ -335,7 +341,7 @@ fn uninstall_editable_by_name() -> Result<()> { ----- stderr ----- Uninstalled 1 package in [TIME] - - poetry-editable==0.1.0 + - poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable/) "###); }); @@ -354,6 +360,12 @@ fn uninstall_editable_by_path() -> Result<()> { let cache_dir = assert_fs::TempDir::new()?; let venv = temp_dir.child(".venv"); + let current_dir = std::env::current_dir()?; + let workspace_dir = current_dir.join("..").join("..").canonicalize()?; + + let mut filters = INSTA_FILTERS.to_vec(); + filters.push((workspace_dir.to_str().unwrap(), "[WORKSPACE_DIR]")); + Command::new(get_cargo_bin(BIN_NAME)) .arg("venv") .arg(venv.as_os_str()) @@ -384,7 +396,7 @@ fn uninstall_editable_by_path() -> Result<()> { // Uninstall the editable by path. insta::with_settings!({ - filters => INSTA_FILTERS.to_vec() + filters => filters.clone() }, { assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) .arg("pip-uninstall") @@ -399,7 +411,7 @@ fn uninstall_editable_by_path() -> Result<()> { ----- stderr ----- Uninstalled 1 package in [TIME] - - poetry-editable==0.1.0 + - poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable/) "###); }); @@ -418,6 +430,12 @@ fn uninstall_duplicate_editable() -> Result<()> { let cache_dir = assert_fs::TempDir::new()?; let venv = temp_dir.child(".venv"); + let current_dir = std::env::current_dir()?; + let workspace_dir = current_dir.join("..").join("..").canonicalize()?; + + let mut filters = INSTA_FILTERS.to_vec(); + filters.push((workspace_dir.to_str().unwrap(), "[WORKSPACE_DIR]")); + Command::new(get_cargo_bin(BIN_NAME)) .arg("venv") .arg(venv.as_os_str()) @@ -448,7 +466,7 @@ fn uninstall_duplicate_editable() -> Result<()> { // Uninstall the editable by both path and name. insta::with_settings!({ - filters => INSTA_FILTERS.to_vec() + filters => filters.clone() }, { assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) .arg("pip-uninstall") @@ -464,7 +482,7 @@ fn uninstall_duplicate_editable() -> Result<()> { ----- stderr ----- Uninstalled 1 package in [TIME] - - poetry-editable==0.1.0 + - poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable/) "###); }); diff --git a/crates/puffin-installer/src/plan.rs b/crates/puffin-installer/src/plan.rs index 2f65eea07..0d4e8136e 100644 --- a/crates/puffin-installer/src/plan.rs +++ b/crates/puffin-installer/src/plan.rs @@ -4,7 +4,7 @@ use anyhow::{bail, Result}; use rustc_hash::FxHashSet; use tracing::{debug, warn}; -use distribution_types::direct_url::{git_reference, DirectUrl}; +use distribution_types::direct_url::git_reference; use distribution_types::{BuiltDist, Dist, SourceDist}; use distribution_types::{CachedDirectUrlDist, CachedDist, InstalledDist, Metadata}; use pep508_rs::{Requirement, VersionOrUrl}; @@ -75,7 +75,7 @@ impl InstallPlan { debug!("Treating editable requirement as immutable: {installed}"); // Remove from the site-packages index, to avoid marking as extraneous. - let Some(editable) = installed.editable() else { + let Some(editable) = installed.as_editable() else { warn!("Editable requirement is not editable: {installed}"); continue; }; @@ -143,17 +143,9 @@ impl InstallPlan { // If the requirement comes from a direct URL, check by URL. Some(VersionOrUrl::Url(url)) => { if let InstalledDist::Url(distribution) = &distribution { - if let Ok(direct_url) = DirectUrl::try_from(url.raw()) { - if let Ok(direct_url) = - pypi_types::DirectUrl::try_from(&direct_url) - { - // TODO(charlie): These don't need to be strictly equal. We only care - // about a subset of the fields. - if direct_url == distribution.url { - debug!("Requirement already satisfied: {distribution}"); - continue; - } - } + if &distribution.url == url.raw() { + debug!("Requirement already satisfied: {distribution}"); + continue; } } } diff --git a/crates/puffin-installer/src/site_packages.rs b/crates/puffin-installer/src/site_packages.rs index 0a6543e44..1eb098823 100644 --- a/crates/puffin-installer/src/site_packages.rs +++ b/crates/puffin-installer/src/site_packages.rs @@ -7,7 +7,7 @@ use url::Url; use distribution_types::{InstalledDist, Metadata, VersionOrUrl}; use pep440_rs::{Version, VersionSpecifiers}; -use pep508_rs::Requirement; +use pep508_rs::{Requirement, VerbatimUrl}; use puffin_interpreter::Virtualenv; use puffin_normalize::PackageName; use requirements_txt::EditableRequirement; @@ -59,7 +59,7 @@ impl<'a> SitePackages<'a> { } // Index the distribution by URL. - if let Some(url) = dist_info.editable() { + if let Some(url) = dist_info.as_editable() { if let Some(existing) = by_url.insert(url.clone(), idx) { let existing = &distributions[existing]; anyhow::bail!( @@ -101,6 +101,9 @@ impl<'a> SitePackages<'a> { )) } VersionOrUrl::Url(url) => pep508_rs::VersionOrUrl::Url(url.clone()), + VersionOrUrl::VersionedUrl(url, ..) => { + pep508_rs::VersionOrUrl::Url(VerbatimUrl::unknown(url.clone())) + } }), marker: None, }) @@ -139,7 +142,7 @@ impl<'a> SitePackages<'a> { if let Some(prev) = self.by_name.get_mut(moved.name()) { *prev = idx; } - if let Some(url) = moved.editable() { + if let Some(url) = moved.as_editable() { if let Some(prev) = self.by_url.get_mut(url) { *prev = idx; } diff --git a/crates/pypi-types/src/direct_url.rs b/crates/pypi-types/src/direct_url.rs index a1881d0fc..b99dd7275 100644 --- a/crates/pypi-types/src/direct_url.rs +++ b/crates/pypi-types/src/direct_url.rs @@ -71,3 +71,49 @@ pub enum VcsKind { Bzr, Svn, } + +impl std::fmt::Display for VcsKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VcsKind::Git => write!(f, "git"), + VcsKind::Hg => write!(f, "hg"), + VcsKind::Bzr => write!(f, "bzr"), + VcsKind::Svn => write!(f, "svn"), + } + } +} + +impl From for Url { + fn from(value: DirectUrl) -> Self { + match value { + DirectUrl::LocalDirectory { url, .. } => url, + DirectUrl::ArchiveUrl { + mut url, + subdirectory, + archive_info: _, + } => { + if let Some(subdirectory) = subdirectory { + url.set_fragment(Some(&format!("subdirectory={}", subdirectory.display()))); + } + url + } + DirectUrl::VcsUrl { + url, + vcs_info, + subdirectory, + } => { + let mut url = + Url::parse(&format!("{}+{}", vcs_info.vcs, url)).expect("VCS URL is invalid"); + if let Some(commit_id) = vcs_info.commit_id { + url.set_path(&format!("{}@{commit_id}", url.path())); + } else if let Some(requested_revision) = vcs_info.requested_revision { + url.set_path(&format!("{}@{requested_revision}", url.path())); + } + if let Some(subdirectory) = subdirectory { + url.set_fragment(Some(&format!("subdirectory={}", subdirectory.display()))); + } + url + } + } + } +}