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:
Charlie Marsh 2023-11-21 11:49:42 +00:00 committed by GitHub
parent f1aa70d9d3
commit 17228ba04e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 580 additions and 24 deletions

1
Cargo.lock generated
View file

@ -2424,6 +2424,7 @@ dependencies = [
"pypi-types",
"pyproject-toml",
"requirements-txt",
"reqwest",
"tempfile",
"textwrap",
"thiserror",

View file

@ -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`
- ...

View file

@ -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,
}),
}
}

View file

@ -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);

View file

@ -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),

View file

@ -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(&registry.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(&registry.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(),
}
}
}

View file

@ -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())

View file

@ -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) {

View file

@ -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.

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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]

View file

@ -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]

View file

@ -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

View file

@ -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

View file

@ -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,
})),
}
}

View file

@ -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,
}
}
}

View file

@ -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);
}
}

View file

@ -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,
},
}
}
}

View file

@ -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()
}

View file

@ -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(_) => {}
}
}

View file

@ -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 {

View file

@ -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};