Skip already-installed dependencies during sync command (#43)

Closes https://github.com/astral-sh/puffin/issues/35.
This commit is contained in:
Charlie Marsh 2023-10-07 15:26:45 -04:00 committed by GitHub
parent bc1736feff
commit 9be02d1590
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 106 additions and 21 deletions

1
Cargo.lock generated
View file

@ -1670,6 +1670,7 @@ name = "puffin-cli"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags 2.4.0",
"cacache", "cacache",
"clap", "clap",
"colored", "colored",

View file

@ -42,7 +42,7 @@ To compare a warm run of `puffin` to `pip`:
```shell ```shell
hyperfine --runs 10 --warmup 3 \ hyperfine --runs 10 --warmup 3 \
"./target/release/puffin-cli sync requirements.txt" \ "./target/release/puffin-cli sync requirements.txt --ignore-installed" \
"pip install -r requirements.txt --ignore-installed --no-deps" "pip install -r requirements.txt --ignore-installed --no-deps"
``` ```
@ -50,10 +50,18 @@ To compare a cold run of `puffin` to `pip`:
```shell ```shell
hyperfine --runs 10 --warmup 3 \ hyperfine --runs 10 --warmup 3 \
"./target/release/puffin-cli sync requirements.txt --no-cache" \ "./target/release/puffin-cli sync requirements.txt --ignore-installed --no-cache" \
"pip install -r requirements.txt --ignore-installed --no-cache-dir --no-deps" "pip install -r requirements.txt --ignore-installed --no-cache-dir --no-deps"
``` ```
To compare a run in which all requirements are already installed:
```shell
hyperfine --runs 10 --warmup 3 \
"./target/release/puffin-cli sync requirements.txt" \
"pip install -r requirements.txt --no-deps"
```
## License ## License
Puffin is licensed under either of Puffin is licensed under either of

View file

@ -15,14 +15,15 @@ platform-host = { path = "../platform-host" }
puffin-resolver = { path = "../puffin-resolver" } puffin-resolver = { path = "../puffin-resolver" }
anyhow = { workspace = true } anyhow = { workspace = true }
bitflags = { workspace = true }
cacache = { workspace = true } cacache = { workspace = true }
clap = { workspace = true, features = ["derive"] } clap = { workspace = true, features = ["derive"] }
colored = { workspace = true } colored = { workspace = true }
directories = { workspace = true } directories = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
tracing = { workspace = true }
tracing-tree = { workspace = true }
tracing-subscriber = { workspace = true }
url = { workspace = true }
tempfile = { workspace = true } tempfile = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-tree = { workspace = true }
url = { workspace = true }

View file

@ -8,6 +8,7 @@ use platform_host::Platform;
use platform_tags::Tags; use platform_tags::Tags;
use puffin_client::PypiClientBuilder; use puffin_client::PypiClientBuilder;
use puffin_interpreter::PythonExecutable; use puffin_interpreter::PythonExecutable;
use puffin_package::requirements::Requirements;
use crate::commands::ExitStatus; use crate::commands::ExitStatus;
@ -17,7 +18,7 @@ pub(crate) async fn compile(src: &Path, cache: Option<&Path>) -> Result<ExitStat
let requirements_txt = std::fs::read_to_string(src)?; let requirements_txt = std::fs::read_to_string(src)?;
// Parse the `requirements.txt` into a list of requirements. // Parse the `requirements.txt` into a list of requirements.
let requirements = puffin_package::requirements::Requirements::from_str(&requirements_txt)?; let requirements = Requirements::from_str(&requirements_txt)?;
// Detect the current Python interpreter. // Detect the current Python interpreter.
let platform = Platform::current()?; let platform = Platform::current()?;

View file

@ -3,7 +3,7 @@ use std::process::ExitCode;
pub(crate) use clean::clean; pub(crate) use clean::clean;
pub(crate) use compile::compile; pub(crate) use compile::compile;
pub(crate) use freeze::freeze; pub(crate) use freeze::freeze;
pub(crate) use sync::sync; pub(crate) use sync::{sync, SyncFlags};
mod clean; mod clean;
mod compile; mod compile;

View file

@ -2,22 +2,33 @@ use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use anyhow::Result; use anyhow::Result;
use bitflags::bitflags;
use tracing::debug; use tracing::debug;
use platform_host::Platform; use platform_host::Platform;
use platform_tags::Tags; use platform_tags::Tags;
use puffin_client::PypiClientBuilder; use puffin_client::PypiClientBuilder;
use puffin_interpreter::PythonExecutable; use puffin_interpreter::{PythonExecutable, SitePackages};
use puffin_package::package_name::PackageName;
use puffin_package::requirements::Requirements;
use crate::commands::ExitStatus; use crate::commands::ExitStatus;
bitflags! {
#[derive(Debug, Copy, Clone, Default)]
pub struct SyncFlags: u8 {
/// Ignore any installed packages, forcing a re-installation.
const IGNORE_INSTALLED = 1 << 0;
}
}
/// Install a set of locked requirements into the current Python environment. /// Install a set of locked requirements into the current Python environment.
pub(crate) async fn sync(src: &Path, cache: Option<&Path>) -> Result<ExitStatus> { pub(crate) async fn sync(src: &Path, cache: Option<&Path>, flags: SyncFlags) -> Result<ExitStatus> {
// Read the `requirements.txt` from disk. // Read the `requirements.txt` from disk.
let requirements_txt = std::fs::read_to_string(src)?; let requirements_txt = std::fs::read_to_string(src)?;
// Parse the `requirements.txt` into a list of requirements. // Parse the `requirements.txt` into a list of requirements.
let requirements = puffin_package::requirements::Requirements::from_str(&requirements_txt)?; let requirements = Requirements::from_str(&requirements_txt)?;
// Detect the current Python interpreter. // Detect the current Python interpreter.
let platform = Platform::current()?; let platform = Platform::current()?;
@ -27,6 +38,29 @@ pub(crate) async fn sync(src: &Path, cache: Option<&Path>) -> Result<ExitStatus>
python.executable().display() python.executable().display()
); );
// Remove any already-installed packages.
let requirements = if flags.intersects(SyncFlags::IGNORE_INSTALLED) {
requirements
} else {
let site_packages = SitePackages::from_executable(&python).await?;
requirements.filter(|requirement| {
let package = PackageName::normalize(&requirement.name);
if let Some(version) = site_packages.get(&package) {
#[allow(clippy::print_stdout)]
{
println!("Requirement already satisfied: {package} ({version})");
}
false
} else {
true
}
})
};
if requirements.is_empty() {
return Ok(ExitStatus::Success);
}
// Determine the current environment markers. // Determine the current environment markers.
let markers = python.markers(); let markers = python.markers();

View file

@ -48,6 +48,10 @@ struct SyncArgs {
/// Avoid reading from or writing to the cache. /// Avoid reading from or writing to the cache.
#[arg(long)] #[arg(long)]
no_cache: bool, no_cache: bool,
/// Ignore any installed packages, forcing a re-installation.
#[arg(long)]
ignore_installed: bool,
} }
#[tokio::main] #[tokio::main]
@ -74,6 +78,11 @@ async fn main() -> ExitCode {
dirs.as_ref() dirs.as_ref()
.map(ProjectDirs::cache_dir) .map(ProjectDirs::cache_dir)
.filter(|_| !args.no_cache), .filter(|_| !args.no_cache),
if args.ignore_installed {
commands::SyncFlags::IGNORE_INSTALLED
} else {
commands::SyncFlags::empty()
},
) )
.await .await
} }

View file

@ -33,6 +33,11 @@ impl SitePackages {
pub fn iter(&self) -> impl Iterator<Item = (&PackageName, &Version)> { pub fn iter(&self) -> impl Iterator<Item = (&PackageName, &Version)> {
self.0.iter() self.0.iter()
} }
/// Returns the version of the given package, if it is installed.
pub fn get(&self, name: &PackageName) -> Option<&Version> {
self.0.get(name)
}
} }
#[derive(Debug)] #[derive(Debug)]

View file

@ -1,5 +1,4 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::ops::Deref;
use std::str::FromStr; use std::str::FromStr;
use anyhow::Result; use anyhow::Result;
@ -10,11 +9,46 @@ use pep508_rs::{Pep508Error, Requirement};
#[derive(Debug)] #[derive(Debug)]
pub struct Requirements(Vec<Requirement>); pub struct Requirements(Vec<Requirement>);
impl Requirements {
pub fn new(requirements: Vec<Requirement>) -> Self {
Self(requirements)
}
/// Filter the requirements.
#[must_use]
pub fn filter<F>(self, mut f: F) -> Self
where
F: FnMut(&Requirement) -> bool,
{
Self(
self.0
.into_iter()
.filter(|requirement| f(requirement))
.collect(),
)
}
/// Return the number of requirements.
pub fn len(&self) -> usize {
self.0.len()
}
/// Return `true` if there are no requirements.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Return an iterator over the requirements.
pub fn iter(&self) -> impl Iterator<Item = &Requirement> {
self.0.iter()
}
}
impl FromStr for Requirements { impl FromStr for Requirements {
type Err = Pep508Error; type Err = Pep508Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self( Ok(Self::new(
RequirementsIterator::new(s) RequirementsIterator::new(s)
.map(|requirement| Requirement::from_str(requirement.as_str())) .map(|requirement| Requirement::from_str(requirement.as_str()))
.collect::<Result<Vec<Requirement>, Pep508Error>>()?, .collect::<Result<Vec<Requirement>, Pep508Error>>()?,
@ -22,14 +56,6 @@ impl FromStr for Requirements {
} }
} }
impl Deref for Requirements {
type Target = [Requirement];
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug)] #[derive(Debug)]
struct RequirementsIterator<'a> { struct RequirementsIterator<'a> {
text: &'a str, text: &'a str,