mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-02 04:48:18 +00:00
Add support for path dependencies (#471)
## Summary This PR adds support for local path dependencies. The approach mostly just falls out of our existing approach and infrastructure for Git and URL dependencies. Closes https://github.com/astral-sh/puffin/issues/436. (We'll open a separate issue for editable installs.) ## Test Plan Added `pip-compile` tests that pre-download a wheel or source distribution, then install it via local path.
This commit is contained in:
parent
f1aa70d9d3
commit
17228ba04e
23 changed files with 580 additions and 24 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2424,6 +2424,7 @@ dependencies = [
|
|||
"pypi-types",
|
||||
"pyproject-toml",
|
||||
"requirements-txt",
|
||||
"reqwest",
|
||||
"tempfile",
|
||||
"textwrap",
|
||||
"thiserror",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ with a minimal barrier to adoption. Try it today in lieu of `pip` and `pip-tools
|
|||
Puffin does not yet support:
|
||||
|
||||
- Windows
|
||||
- Path dependencies
|
||||
- Editable installs (`pip install -e ...`)
|
||||
- Package-less requirements (`pip install https://...`)
|
||||
- `--find-links`
|
||||
- ...
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,11 @@ impl CachedDist {
|
|||
url: dist.url,
|
||||
path,
|
||||
}),
|
||||
Dist::Built(BuiltDist::Path(dist)) => Self::Url(CachedDirectUrlDist {
|
||||
name: dist.filename.name,
|
||||
url: dist.url,
|
||||
path,
|
||||
}),
|
||||
Dist::Source(SourceDist::Registry(dist)) => Self::Registry(CachedRegistryDist {
|
||||
name: dist.name,
|
||||
version: dist.version,
|
||||
|
|
@ -99,6 +104,11 @@ impl CachedDist {
|
|||
url: dist.url,
|
||||
path,
|
||||
}),
|
||||
Dist::Source(SourceDist::Path(dist)) => Self::Url(CachedDirectUrlDist {
|
||||
name: dist.name,
|
||||
url: dist.url,
|
||||
path,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,19 @@ use puffin_git::GitUrl;
|
|||
|
||||
#[derive(Debug)]
|
||||
pub enum DirectUrl {
|
||||
/// The direct URL is a path to a local directory or file.
|
||||
LocalFile(LocalFileUrl),
|
||||
/// The direct URL is path to a Git repository.
|
||||
Git(DirectGitUrl),
|
||||
/// The direct URL is a URL to an archive.
|
||||
Archive(DirectArchiveUrl),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LocalFileUrl {
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DirectGitUrl {
|
||||
pub url: GitUrl,
|
||||
|
|
@ -23,6 +32,12 @@ pub struct DirectArchiveUrl {
|
|||
pub subdirectory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl From<&Url> for LocalFileUrl {
|
||||
fn from(url: &Url) -> Self {
|
||||
Self { url: url.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Url> for DirectGitUrl {
|
||||
type Error = Error;
|
||||
|
||||
|
|
@ -73,6 +88,8 @@ impl TryFrom<&Url> for DirectUrl {
|
|||
"Unsupported URL prefix `{prefix}` in URL: {url}",
|
||||
))),
|
||||
}
|
||||
} else if url.scheme().eq_ignore_ascii_case("file") {
|
||||
Ok(Self::LocalFile(LocalFileUrl::from(url)))
|
||||
} else {
|
||||
Ok(Self::Archive(DirectArchiveUrl::from(url)))
|
||||
}
|
||||
|
|
@ -84,12 +101,24 @@ impl TryFrom<&DirectUrl> for pypi_types::DirectUrl {
|
|||
|
||||
fn try_from(value: &DirectUrl) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
DirectUrl::LocalFile(value) => pypi_types::DirectUrl::try_from(value),
|
||||
DirectUrl::Git(value) => pypi_types::DirectUrl::try_from(value),
|
||||
DirectUrl::Archive(value) => pypi_types::DirectUrl::try_from(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&LocalFileUrl> for pypi_types::DirectUrl {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: &LocalFileUrl) -> Result<Self, Self::Error> {
|
||||
Ok(pypi_types::DirectUrl::LocalDirectory {
|
||||
url: value.url.to_string(),
|
||||
dir_info: pypi_types::DirInfo { editable: None },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DirectArchiveUrl> for pypi_types::DirectUrl {
|
||||
type Error = Error;
|
||||
|
||||
|
|
@ -124,12 +153,19 @@ impl TryFrom<&DirectGitUrl> for pypi_types::DirectUrl {
|
|||
impl From<DirectUrl> for Url {
|
||||
fn from(value: DirectUrl) -> Self {
|
||||
match value {
|
||||
DirectUrl::LocalFile(value) => value.into(),
|
||||
DirectUrl::Git(value) => value.into(),
|
||||
DirectUrl::Archive(value) => value.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LocalFileUrl> for Url {
|
||||
fn from(value: LocalFileUrl) -> Self {
|
||||
value.url
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DirectArchiveUrl> for Url {
|
||||
fn from(value: DirectArchiveUrl) -> Self {
|
||||
let mut url = value.url;
|
||||
|
|
@ -160,6 +196,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn direct_url_from_url() -> Result<()> {
|
||||
let expected = Url::parse("file:///path/to/directory")?;
|
||||
let actual = Url::from(DirectUrl::try_from(&expected)?);
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
let expected = Url::parse("git+https://github.com/pallets/flask.git")?;
|
||||
let actual = Url::from(DirectUrl::try_from(&expected)?);
|
||||
assert_eq!(expected, actual);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ use url::Url;
|
|||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
WheelFilename(#[from] distribution_filename::WheelFilenameError),
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use distribution_filename::WheelFilename;
|
||||
use url::Url;
|
||||
|
||||
use distribution_filename::WheelFilename;
|
||||
use pep440_rs::Version;
|
||||
use puffin_normalize::PackageName;
|
||||
use pypi_types::{File, IndexUrl};
|
||||
|
|
@ -50,6 +50,7 @@ pub enum Dist {
|
|||
pub enum BuiltDist {
|
||||
Registry(RegistryBuiltDist),
|
||||
DirectUrl(DirectUrlBuiltDist),
|
||||
Path(PathBuiltDist),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -58,6 +59,7 @@ pub enum SourceDist {
|
|||
Registry(RegistrySourceDist),
|
||||
DirectUrl(DirectUrlSourceDist),
|
||||
Git(GitSourceDist),
|
||||
Path(PathSourceDist),
|
||||
}
|
||||
|
||||
/// A built distribution (wheel) that exists in a registry, like `PyPI`.
|
||||
|
|
@ -78,6 +80,14 @@ pub struct DirectUrlBuiltDist {
|
|||
pub url: Url,
|
||||
}
|
||||
|
||||
/// A built distribution (wheel) that exists in a local directory.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PathBuiltDist {
|
||||
pub filename: WheelFilename,
|
||||
pub url: Url,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
/// A source distribution that exists in a registry, like `PyPI`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RegistrySourceDist {
|
||||
|
|
@ -103,6 +113,14 @@ pub struct GitSourceDist {
|
|||
pub url: Url,
|
||||
}
|
||||
|
||||
/// A source distribution that exists in a local directory.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PathSourceDist {
|
||||
pub name: PackageName,
|
||||
pub url: Url,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl Dist {
|
||||
/// Create a [`Dist`] for a registry-based distribution.
|
||||
pub fn from_registry(name: PackageName, version: Version, file: File, index: IndexUrl) -> Self {
|
||||
|
|
@ -129,8 +147,34 @@ impl Dist {
|
|||
/// Create a [`Dist`] for a URL-based distribution.
|
||||
pub fn from_url(name: PackageName, url: Url) -> Result<Self, Error> {
|
||||
if url.scheme().starts_with("git+") {
|
||||
Ok(Self::Source(SourceDist::Git(GitSourceDist { name, url })))
|
||||
} else if Path::new(url.path())
|
||||
return Ok(Self::Source(SourceDist::Git(GitSourceDist { name, url })));
|
||||
}
|
||||
|
||||
if url.scheme().eq_ignore_ascii_case("file") {
|
||||
// Store the canonicalized path.
|
||||
let path = url
|
||||
.to_file_path()
|
||||
.map_err(|()| Error::UrlFilename(url.clone()))?
|
||||
.canonicalize()?;
|
||||
return if path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("whl"))
|
||||
{
|
||||
Ok(Self::Built(BuiltDist::Path(PathBuiltDist {
|
||||
filename: WheelFilename::from_str(url.filename()?)?,
|
||||
url,
|
||||
path,
|
||||
})))
|
||||
} else {
|
||||
Ok(Self::Source(SourceDist::Path(PathSourceDist {
|
||||
name,
|
||||
url,
|
||||
path,
|
||||
})))
|
||||
};
|
||||
}
|
||||
|
||||
if Path::new(url.path())
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("whl"))
|
||||
{
|
||||
|
|
@ -158,21 +202,18 @@ impl Dist {
|
|||
pub fn with_url(self, url: Url) -> Self {
|
||||
match self {
|
||||
Self::Built(built) => Self::Built(match built {
|
||||
BuiltDist::DirectUrl(dist) => BuiltDist::DirectUrl(DirectUrlBuiltDist {
|
||||
filename: dist.filename,
|
||||
url,
|
||||
}),
|
||||
BuiltDist::DirectUrl(dist) => {
|
||||
BuiltDist::DirectUrl(DirectUrlBuiltDist { url, ..dist })
|
||||
}
|
||||
BuiltDist::Path(dist) => BuiltDist::Path(PathBuiltDist { url, ..dist }),
|
||||
dist @ BuiltDist::Registry(_) => dist,
|
||||
}),
|
||||
Self::Source(source) => Self::Source(match source {
|
||||
SourceDist::DirectUrl(dist) => SourceDist::DirectUrl(DirectUrlSourceDist {
|
||||
name: dist.name,
|
||||
url,
|
||||
}),
|
||||
SourceDist::Git(dist) => SourceDist::Git(GitSourceDist {
|
||||
name: dist.name,
|
||||
url,
|
||||
}),
|
||||
SourceDist::DirectUrl(dist) => {
|
||||
SourceDist::DirectUrl(DirectUrlSourceDist { url, ..dist })
|
||||
}
|
||||
SourceDist::Git(dist) => SourceDist::Git(GitSourceDist { url, ..dist }),
|
||||
SourceDist::Path(dist) => SourceDist::Path(PathSourceDist { url, ..dist }),
|
||||
dist @ SourceDist::Registry(_) => dist,
|
||||
}),
|
||||
}
|
||||
|
|
@ -184,7 +225,7 @@ impl BuiltDist {
|
|||
pub fn file(&self) -> Option<&File> {
|
||||
match self {
|
||||
BuiltDist::Registry(registry) => Some(®istry.file),
|
||||
BuiltDist::DirectUrl(_) => None,
|
||||
BuiltDist::DirectUrl(_) | BuiltDist::Path(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -194,7 +235,7 @@ impl SourceDist {
|
|||
pub fn file(&self) -> Option<&File> {
|
||||
match self {
|
||||
SourceDist::Registry(registry) => Some(®istry.file),
|
||||
SourceDist::DirectUrl(_) | SourceDist::Git(_) => None,
|
||||
SourceDist::DirectUrl(_) | SourceDist::Git(_) | SourceDist::Path(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -219,6 +260,16 @@ impl Metadata for DirectUrlBuiltDist {
|
|||
}
|
||||
}
|
||||
|
||||
impl Metadata for PathBuiltDist {
|
||||
fn name(&self) -> &PackageName {
|
||||
&self.filename.name
|
||||
}
|
||||
|
||||
fn version_or_url(&self) -> VersionOrUrl {
|
||||
VersionOrUrl::Url(&self.url)
|
||||
}
|
||||
}
|
||||
|
||||
impl Metadata for RegistrySourceDist {
|
||||
fn name(&self) -> &PackageName {
|
||||
&self.name
|
||||
|
|
@ -249,12 +300,23 @@ impl Metadata for GitSourceDist {
|
|||
}
|
||||
}
|
||||
|
||||
impl Metadata for PathSourceDist {
|
||||
fn name(&self) -> &PackageName {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn version_or_url(&self) -> VersionOrUrl {
|
||||
VersionOrUrl::Url(&self.url)
|
||||
}
|
||||
}
|
||||
|
||||
impl Metadata for SourceDist {
|
||||
fn name(&self) -> &PackageName {
|
||||
match self {
|
||||
Self::Registry(dist) => dist.name(),
|
||||
Self::DirectUrl(dist) => dist.name(),
|
||||
Self::Git(dist) => dist.name(),
|
||||
Self::Path(dist) => dist.name(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -263,6 +325,7 @@ impl Metadata for SourceDist {
|
|||
Self::Registry(dist) => dist.version_or_url(),
|
||||
Self::DirectUrl(dist) => dist.version_or_url(),
|
||||
Self::Git(dist) => dist.version_or_url(),
|
||||
Self::Path(dist) => dist.version_or_url(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -272,6 +335,7 @@ impl Metadata for BuiltDist {
|
|||
match self {
|
||||
Self::Registry(dist) => dist.name(),
|
||||
Self::DirectUrl(dist) => dist.name(),
|
||||
Self::Path(dist) => dist.name(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -279,6 +343,7 @@ impl Metadata for BuiltDist {
|
|||
match self {
|
||||
Self::Registry(dist) => dist.version_or_url(),
|
||||
Self::DirectUrl(dist) => dist.version_or_url(),
|
||||
Self::Path(dist) => dist.version_or_url(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -375,12 +440,33 @@ impl RemoteSource for GitSourceDist {
|
|||
}
|
||||
}
|
||||
|
||||
impl RemoteSource for PathBuiltDist {
|
||||
fn filename(&self) -> Result<&str, Error> {
|
||||
self.url.filename()
|
||||
}
|
||||
|
||||
fn size(&self) -> Option<usize> {
|
||||
self.url.size()
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteSource for PathSourceDist {
|
||||
fn filename(&self) -> Result<&str, Error> {
|
||||
self.url.filename()
|
||||
}
|
||||
|
||||
fn size(&self) -> Option<usize> {
|
||||
self.url.size()
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteSource for SourceDist {
|
||||
fn filename(&self) -> Result<&str, Error> {
|
||||
match self {
|
||||
Self::Registry(dist) => dist.filename(),
|
||||
Self::DirectUrl(dist) => dist.filename(),
|
||||
Self::Git(dist) => dist.filename(),
|
||||
Self::Path(dist) => dist.filename(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -389,6 +475,7 @@ impl RemoteSource for SourceDist {
|
|||
Self::Registry(dist) => dist.size(),
|
||||
Self::DirectUrl(dist) => dist.size(),
|
||||
Self::Git(dist) => dist.size(),
|
||||
Self::Path(dist) => dist.size(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -398,6 +485,7 @@ impl RemoteSource for BuiltDist {
|
|||
match self {
|
||||
Self::Registry(dist) => dist.filename(),
|
||||
Self::DirectUrl(dist) => dist.filename(),
|
||||
Self::Path(dist) => dist.filename(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -405,6 +493,7 @@ impl RemoteSource for BuiltDist {
|
|||
match self {
|
||||
Self::Registry(dist) => dist.size(),
|
||||
Self::DirectUrl(dist) => dist.size(),
|
||||
Self::Path(dist) => dist.size(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -445,6 +534,16 @@ impl Identifier for File {
|
|||
}
|
||||
}
|
||||
|
||||
impl Identifier for Path {
|
||||
fn distribution_id(&self) -> String {
|
||||
puffin_cache::digest(&self)
|
||||
}
|
||||
|
||||
fn resource_id(&self) -> String {
|
||||
puffin_cache::digest(&self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Identifier for RegistryBuiltDist {
|
||||
fn distribution_id(&self) -> String {
|
||||
self.file.distribution_id()
|
||||
|
|
@ -485,6 +584,26 @@ impl Identifier for DirectUrlSourceDist {
|
|||
}
|
||||
}
|
||||
|
||||
impl Identifier for PathBuiltDist {
|
||||
fn distribution_id(&self) -> String {
|
||||
self.url.distribution_id()
|
||||
}
|
||||
|
||||
fn resource_id(&self) -> String {
|
||||
self.url.resource_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl Identifier for PathSourceDist {
|
||||
fn distribution_id(&self) -> String {
|
||||
self.url.distribution_id()
|
||||
}
|
||||
|
||||
fn resource_id(&self) -> String {
|
||||
self.url.resource_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl Identifier for GitSourceDist {
|
||||
fn distribution_id(&self) -> String {
|
||||
self.url.distribution_id()
|
||||
|
|
@ -501,6 +620,7 @@ impl Identifier for SourceDist {
|
|||
Self::Registry(dist) => dist.distribution_id(),
|
||||
Self::DirectUrl(dist) => dist.distribution_id(),
|
||||
Self::Git(dist) => dist.distribution_id(),
|
||||
Self::Path(dist) => dist.distribution_id(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -509,6 +629,7 @@ impl Identifier for SourceDist {
|
|||
Self::Registry(dist) => dist.resource_id(),
|
||||
Self::DirectUrl(dist) => dist.resource_id(),
|
||||
Self::Git(dist) => dist.resource_id(),
|
||||
Self::Path(dist) => dist.resource_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -518,6 +639,7 @@ impl Identifier for BuiltDist {
|
|||
match self {
|
||||
Self::Registry(dist) => dist.distribution_id(),
|
||||
Self::DirectUrl(dist) => dist.distribution_id(),
|
||||
Self::Path(dist) => dist.distribution_id(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -525,6 +647,7 @@ impl Identifier for BuiltDist {
|
|||
match self {
|
||||
Self::Registry(dist) => dist.resource_id(),
|
||||
Self::DirectUrl(dist) => dist.resource_id(),
|
||||
Self::Path(dist) => dist.resource_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ use crate::error::Error;
|
|||
use crate::{
|
||||
AnyDist, BuiltDist, CachedDirectUrlDist, CachedDist, CachedRegistryDist, DirectUrlBuiltDist,
|
||||
DirectUrlSourceDist, Dist, GitSourceDist, InstalledDirectUrlDist, InstalledDist,
|
||||
InstalledRegistryDist, RegistryBuiltDist, RegistrySourceDist, SourceDist, VersionOrUrl,
|
||||
InstalledRegistryDist, PathBuiltDist, PathSourceDist, RegistryBuiltDist, RegistrySourceDist,
|
||||
SourceDist, VersionOrUrl,
|
||||
};
|
||||
|
||||
pub trait Metadata {
|
||||
|
|
@ -129,6 +130,18 @@ impl std::fmt::Display for InstalledRegistryDist {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PathBuiltDist {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}{}", self.name(), self.version_or_url())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PathSourceDist {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}{}", self.name(), self.version_or_url())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RegistryBuiltDist {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}{}", self.name(), self.version_or_url())
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use std::num::{
|
|||
NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroU128, NonZeroU16,
|
||||
NonZeroU32, NonZeroU64, NonZeroU8,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use seahash::SeaHasher;
|
||||
|
||||
|
|
@ -196,6 +197,20 @@ impl CacheKey for String {
|
|||
}
|
||||
}
|
||||
|
||||
impl CacheKey for Path {
|
||||
#[inline]
|
||||
fn cache_key(&self, state: &mut CacheKeyHasher) {
|
||||
self.hash(&mut *state);
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheKey for PathBuf {
|
||||
#[inline]
|
||||
fn cache_key(&self, state: &mut CacheKeyHasher) {
|
||||
self.as_path().cache_key(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: CacheKey> CacheKey for Option<T> {
|
||||
#[inline]
|
||||
fn cache_key(&self, state: &mut CacheKeyHasher) {
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ assert_fs = { version = "1.0.13" }
|
|||
insta-cmd = { version = "0.4.0" }
|
||||
insta = { version = "1.34.0", features = ["filters"] }
|
||||
predicates = { version = "3.0.4" }
|
||||
reqwest = { version = "0.11.22", features = ["blocking", "rustls"], default-features = false }
|
||||
|
||||
[features]
|
||||
# Introduces a dependency on a local Python installation.
|
||||
|
|
|
|||
|
|
@ -1421,3 +1421,77 @@ fn compile_exclude_newer() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve a local path dependency on a specific wheel.
|
||||
#[test]
|
||||
fn compile_wheel_path_dependency() -> Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let cache_dir = TempDir::new()?;
|
||||
let venv = make_venv_py312(&temp_dir, &cache_dir);
|
||||
|
||||
// Download a wheel.
|
||||
let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl")?;
|
||||
let flask_wheel = temp_dir.child("flask-3.0.0-py3-none-any.whl");
|
||||
let mut flask_wheel_file = std::fs::File::create(&flask_wheel)?;
|
||||
std::io::copy(&mut response.bytes()?.as_ref(), &mut flask_wheel_file)?;
|
||||
|
||||
let requirements_in = temp_dir.child("requirements.in");
|
||||
requirements_in.write_str(&format!("flask @ file://{}", flask_wheel.path().display()))?;
|
||||
|
||||
// In addition to the standard filters, remove the temporary directory from the snapshot.
|
||||
let mut filters = INSTA_FILTERS.to_vec();
|
||||
filters.push((r"file://.*/", "file://[TEMP_DIR]/"));
|
||||
|
||||
insta::with_settings!({
|
||||
filters => filters
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.arg("pip-compile")
|
||||
.arg("requirements.in")
|
||||
.arg("--cache-dir")
|
||||
.arg(cache_dir.path())
|
||||
.arg("--exclude-newer")
|
||||
.arg(EXCLUDE_NEWER)
|
||||
.env("VIRTUAL_ENV", venv.as_os_str())
|
||||
.current_dir(&temp_dir));
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve a local path dependency on a specific source distribution.
|
||||
#[test]
|
||||
fn compile_source_distribution_path_dependency() -> Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let cache_dir = TempDir::new()?;
|
||||
let venv = make_venv_py312(&temp_dir, &cache_dir);
|
||||
|
||||
// Download a source distribution.
|
||||
let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/d8/09/c1a7354d3925a3c6c8cfdebf4245bae67d633ffda1ba415add06ffc839c5/flask-3.0.0.tar.gz")?;
|
||||
let flask_wheel = temp_dir.child("flask-3.0.0.tar.gz");
|
||||
let mut flask_wheel_file = std::fs::File::create(&flask_wheel)?;
|
||||
std::io::copy(&mut response.bytes()?.as_ref(), &mut flask_wheel_file)?;
|
||||
|
||||
let requirements_in = temp_dir.child("requirements.in");
|
||||
requirements_in.write_str(&format!("flask @ file://{}", flask_wheel.path().display()))?;
|
||||
|
||||
// In addition to the standard filters, remove the temporary directory from the snapshot.
|
||||
let mut filters = INSTA_FILTERS.to_vec();
|
||||
filters.push((r"file://.*/", "file://[TEMP_DIR]/"));
|
||||
|
||||
insta::with_settings!({
|
||||
filters => filters
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.arg("pip-compile")
|
||||
.arg("requirements.in")
|
||||
.arg("--cache-dir")
|
||||
.arg(cache_dir.path())
|
||||
.arg("--exclude-newer")
|
||||
.arg(EXCLUDE_NEWER)
|
||||
.env("VIRTUAL_ENV", venv.as_os_str())
|
||||
.current_dir(&temp_dir));
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1049,3 +1049,97 @@ fn warn_on_yanked_version() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve a local wheel.
|
||||
#[test]
|
||||
fn install_local_wheel() -> Result<()> {
|
||||
let temp_dir = assert_fs::TempDir::new()?;
|
||||
let cache_dir = assert_fs::TempDir::new()?;
|
||||
let venv = temp_dir.child(".venv");
|
||||
|
||||
Command::new(get_cargo_bin(BIN_NAME))
|
||||
.arg("venv")
|
||||
.arg(venv.as_os_str())
|
||||
.arg("--cache-dir")
|
||||
.arg(cache_dir.path())
|
||||
.arg("--python")
|
||||
.arg("python3.12")
|
||||
.current_dir(&temp_dir)
|
||||
.assert()
|
||||
.success();
|
||||
venv.assert(predicates::path::is_dir());
|
||||
|
||||
// Download a wheel.
|
||||
let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl")?;
|
||||
let flask_wheel = temp_dir.child("flask-3.0.0-py3-none-any.whl");
|
||||
let mut flask_wheel_file = std::fs::File::create(&flask_wheel)?;
|
||||
std::io::copy(&mut response.bytes()?.as_ref(), &mut flask_wheel_file)?;
|
||||
|
||||
let requirements_txt = temp_dir.child("requirements.txt");
|
||||
requirements_txt.write_str(&format!("flask @ file://{}", flask_wheel.path().display()))?;
|
||||
|
||||
// In addition to the standard filters, remove the temporary directory from the snapshot.
|
||||
let mut filters = INSTA_FILTERS.to_vec();
|
||||
filters.push((r"file://.*/", "file://[TEMP_DIR]/"));
|
||||
|
||||
insta::with_settings!({
|
||||
filters => filters
|
||||
}, {
|
||||
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));
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install a local source distribution.
|
||||
#[test]
|
||||
fn install_local_source_distribution() -> Result<()> {
|
||||
let temp_dir = assert_fs::TempDir::new()?;
|
||||
let cache_dir = assert_fs::TempDir::new()?;
|
||||
let venv = temp_dir.child(".venv");
|
||||
|
||||
Command::new(get_cargo_bin(BIN_NAME))
|
||||
.arg("venv")
|
||||
.arg(venv.as_os_str())
|
||||
.arg("--cache-dir")
|
||||
.arg(cache_dir.path())
|
||||
.arg("--python")
|
||||
.arg("python3.12")
|
||||
.current_dir(&temp_dir)
|
||||
.assert()
|
||||
.success();
|
||||
venv.assert(predicates::path::is_dir());
|
||||
|
||||
// Download a source distribution.
|
||||
let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/d8/09/c1a7354d3925a3c6c8cfdebf4245bae67d633ffda1ba415add06ffc839c5/flask-3.0.0.tar.gz")?;
|
||||
let flask_wheel = temp_dir.child("flask-3.0.0.tar.gz");
|
||||
let mut flask_wheel_file = std::fs::File::create(&flask_wheel)?;
|
||||
std::io::copy(&mut response.bytes()?.as_ref(), &mut flask_wheel_file)?;
|
||||
|
||||
let requirements_txt = temp_dir.child("requirements.txt");
|
||||
requirements_txt.write_str(&format!("flask @ file://{}", flask_wheel.path().display()))?;
|
||||
|
||||
// In addition to the standard filters, remove the temporary directory from the snapshot.
|
||||
let mut filters = INSTA_FILTERS.to_vec();
|
||||
filters.push((r"file://.*/", "file://[TEMP_DIR]/"));
|
||||
|
||||
insta::with_settings!({
|
||||
filters => filters
|
||||
}, {
|
||||
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));
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
source: crates/puffin-cli/tests/pip_compile.rs
|
||||
info:
|
||||
program: puffin
|
||||
args:
|
||||
- pip-compile
|
||||
- requirements.in
|
||||
- "--cache-dir"
|
||||
- /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpz0R3e9
|
||||
- "--exclude-newer"
|
||||
- "2023-11-18T12:00:00Z"
|
||||
env:
|
||||
VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmp2xo89r/.venv
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by Puffin v0.0.1 via the following command:
|
||||
# puffin pip-compile requirements.in --cache-dir [CACHE_DIR]
|
||||
blinker==1.7.0
|
||||
# via flask
|
||||
click==8.1.7
|
||||
# via flask
|
||||
flask @ file://[TEMP_DIR]/flask-3.0.0.tar.gz
|
||||
itsdangerous==2.1.2
|
||||
# via flask
|
||||
jinja2==3.1.2
|
||||
# via flask
|
||||
markupsafe==2.1.3
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
werkzeug==3.0.1
|
||||
# via flask
|
||||
|
||||
----- stderr -----
|
||||
Resolved 7 packages in [TIME]
|
||||
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
source: crates/puffin-cli/tests/pip_compile.rs
|
||||
info:
|
||||
program: puffin
|
||||
args:
|
||||
- pip-compile
|
||||
- requirements.in
|
||||
- "--cache-dir"
|
||||
- /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpKtoCCH
|
||||
- "--exclude-newer"
|
||||
- "2023-11-18T12:00:00Z"
|
||||
env:
|
||||
VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmp0htf0U/.venv
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by Puffin v0.0.1 via the following command:
|
||||
# puffin pip-compile requirements.in --cache-dir [CACHE_DIR]
|
||||
blinker==1.7.0
|
||||
# via flask
|
||||
click==8.1.7
|
||||
# via flask
|
||||
flask @ file://[TEMP_DIR]/flask-3.0.0-py3-none-any.whl
|
||||
itsdangerous==2.1.2
|
||||
# via flask
|
||||
jinja2==3.1.2
|
||||
# via flask
|
||||
markupsafe==2.1.3
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
werkzeug==3.0.1
|
||||
# via flask
|
||||
|
||||
----- stderr -----
|
||||
Resolved 7 packages in [TIME]
|
||||
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
source: crates/puffin-cli/tests/pip_sync.rs
|
||||
info:
|
||||
program: puffin
|
||||
args:
|
||||
- pip-sync
|
||||
- requirements.txt
|
||||
- "--cache-dir"
|
||||
- /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmp1HfKKY
|
||||
env:
|
||||
VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpBmYNQk/.venv
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
Downloaded 1 package in [TIME]
|
||||
Built 1 package in [TIME]
|
||||
Unzipped 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ flask @ file://[TEMP_DIR]/flask-3.0.0.tar.gz
|
||||
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
source: crates/puffin-cli/tests/pip_sync.rs
|
||||
info:
|
||||
program: puffin
|
||||
args:
|
||||
- pip-sync
|
||||
- requirements.txt
|
||||
- "--cache-dir"
|
||||
- /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpKx7cY5
|
||||
env:
|
||||
VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpEz5kWW/.venv
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
Downloaded 1 package in [TIME]
|
||||
Unzipped 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ flask @ file://[TEMP_DIR]/flask-3.0.0-py3-none-any.whl
|
||||
|
||||
|
|
@ -181,6 +181,14 @@ impl<'a> Fetcher<'a> {
|
|||
})))
|
||||
}
|
||||
|
||||
Dist::Built(BuiltDist::Path(wheel)) => {
|
||||
Ok(Download::Wheel(WheelDownload::Disk(DiskWheel {
|
||||
dist: dist.clone(),
|
||||
path: wheel.path.clone(),
|
||||
temp_dir: None,
|
||||
})))
|
||||
}
|
||||
|
||||
Dist::Source(SourceDist::Registry(sdist)) => {
|
||||
debug!(
|
||||
"Fetching source distribution from registry: {}",
|
||||
|
|
@ -246,6 +254,13 @@ impl<'a> Fetcher<'a> {
|
|||
temp_dir: None,
|
||||
}))
|
||||
}
|
||||
|
||||
Dist::Source(SourceDist::Path(sdist)) => Ok(Download::SourceDist(SourceDistDownload {
|
||||
dist: dist.clone(),
|
||||
sdist_file: sdist.path.clone(),
|
||||
subdirectory: None,
|
||||
temp_dir: None,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ impl WheelCache {
|
|||
pub(crate) enum CacheShard {
|
||||
Registry,
|
||||
Url,
|
||||
Local,
|
||||
}
|
||||
|
||||
impl CacheShard {
|
||||
|
|
@ -54,6 +55,7 @@ impl CacheShard {
|
|||
match self {
|
||||
Self::Registry => "registry",
|
||||
Self::Url => "url",
|
||||
Self::Local => "local",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -63,9 +65,11 @@ impl From<&Dist> for CacheShard {
|
|||
match dist {
|
||||
Dist::Built(BuiltDist::Registry(_)) => Self::Registry,
|
||||
Dist::Built(BuiltDist::DirectUrl(_)) => Self::Url,
|
||||
Dist::Built(BuiltDist::Path(_)) => Self::Local,
|
||||
Dist::Source(SourceDist::Registry(_)) => Self::Registry,
|
||||
Dist::Source(SourceDist::DirectUrl(_)) => Self::Url,
|
||||
Dist::Source(SourceDist::Git(_)) => Self::Url,
|
||||
Dist::Source(SourceDist::Path(_)) => Self::Local,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use fs_err as fs;
|
||||
|
||||
use distribution_types::{InstalledDist, Metadata};
|
||||
|
|
@ -18,7 +18,11 @@ impl SitePackages {
|
|||
for entry in fs::read_dir(venv.site_packages())? {
|
||||
let entry = entry?;
|
||||
if entry.file_type()?.is_dir() {
|
||||
if let Some(dist_info) = InstalledDist::try_from_path(&entry.path())? {
|
||||
if let Some(dist_info) =
|
||||
InstalledDist::try_from_path(&entry.path()).with_context(|| {
|
||||
format!("Failed to read metadata: from {}", entry.path().display())
|
||||
})?
|
||||
{
|
||||
index.insert(dist_info.name().clone(), dist_info);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,10 @@ impl ResolveError {
|
|||
url: wheel.url.clone(),
|
||||
err,
|
||||
},
|
||||
Dist::Built(BuiltDist::Path(wheel)) => Self::UrlBuiltDist {
|
||||
url: wheel.url.clone(),
|
||||
err,
|
||||
},
|
||||
Dist::Source(SourceDist::Registry(sdist)) => Self::RegistrySourceDist {
|
||||
filename: sdist.file.filename.clone(),
|
||||
err,
|
||||
|
|
@ -140,6 +144,10 @@ impl ResolveError {
|
|||
url: sdist.url.clone(),
|
||||
err,
|
||||
},
|
||||
Dist::Source(SourceDist::Path(sdist)) => Self::UrlBuiltDist {
|
||||
url: sdist.url.clone(),
|
||||
err,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,6 +163,12 @@ impl Graph {
|
|||
version_or_url: Some(VersionOrUrl::Url(wheel.url.clone())),
|
||||
marker: None,
|
||||
},
|
||||
Dist::Built(BuiltDist::Path(wheel)) => Requirement {
|
||||
name: wheel.filename.name.clone(),
|
||||
extras: None,
|
||||
version_or_url: Some(VersionOrUrl::Url(wheel.url.clone())),
|
||||
marker: None,
|
||||
},
|
||||
Dist::Source(SourceDist::Registry(sdist)) => Requirement {
|
||||
name: sdist.name.clone(),
|
||||
extras: None,
|
||||
|
|
@ -183,6 +189,12 @@ impl Graph {
|
|||
version_or_url: Some(VersionOrUrl::Url(sdist.url.clone())),
|
||||
marker: None,
|
||||
},
|
||||
Dist::Source(SourceDist::Path(sdist)) => Requirement {
|
||||
name: sdist.name.clone(),
|
||||
extras: None,
|
||||
version_or_url: Some(VersionOrUrl::Url(sdist.url.clone())),
|
||||
marker: None,
|
||||
},
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -578,6 +578,9 @@ impl<'a, Context: BuildContext + Send + Sync> Resolver<'a, Context> {
|
|||
SourceDist::Git(sdist) => {
|
||||
self.index.redirects.insert(sdist.url.clone(), precise);
|
||||
}
|
||||
SourceDist::Path(sdist) => {
|
||||
self.index.redirects.insert(sdist.url.clone(), precise);
|
||||
}
|
||||
SourceDist::Registry(_) => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,13 @@ use serde::{Deserialize, Serialize};
|
|||
///
|
||||
/// See: <https://packaging.python.org/en/latest/specifications/direct-url-data-structure/>
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[serde(rename_all = "snake_case", untagged)]
|
||||
pub enum DirectUrl {
|
||||
/// The direct URL is a local directory. For example:
|
||||
/// ```json
|
||||
/// {"url": "file:///home/user/project", "dir_info": {}}
|
||||
/// ```
|
||||
LocalDirectory { url: String, dir_info: DirInfo },
|
||||
/// The direct URL is a path to an archive. For example:
|
||||
/// ```json
|
||||
/// {"archive_info": {"hash": "sha256=75909db2664838d015e3d9139004ee16711748a52c8f336b52882266540215d8", "hashes": {"sha256": "75909db2664838d015e3d9139004ee16711748a52c8f336b52882266540215d8"}}, "url": "https://files.pythonhosted.org/packages/b8/8b/31273bf66016be6ad22bb7345c37ff350276cfd46e389a0c2ac5da9d9073/wheel-0.41.2-py3-none-any.whl"}
|
||||
|
|
@ -31,6 +36,13 @@ pub enum DirectUrl {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct DirInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub editable: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ArchiveInfo {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
pub use direct_url::{ArchiveInfo, DirectUrl, VcsInfo, VcsKind};
|
||||
pub use direct_url::{ArchiveInfo, DirInfo, DirectUrl, VcsInfo, VcsKind};
|
||||
pub use index_url::IndexUrl;
|
||||
pub use lenient_requirement::LenientVersionSpecifiers;
|
||||
pub use metadata::{Error, Metadata21};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue