Add support for `--no-deps` to `pip install` (#1191)

## Summary

Closes https://github.com/astral-sh/puffin/issues/1188.
This commit is contained in:
Charlie Marsh 2024-01-30 11:54:57 -08:00 committed by GitHub
parent 8305acc584
commit c129717b41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 102 additions and 5 deletions

View File

@ -0,0 +1,20 @@
#[derive(Debug, Default, Clone, Copy)]
pub enum DependencyMode {
/// Include all dependencies, whether direct or transitive.
#[default]
Transitive,
/// Exclude transitive dependencies, only resolving the root package's immediate dependencies.
Direct,
}
impl DependencyMode {
/// Returns `true` if transitive dependencies should be included.
pub fn is_transitive(self) -> bool {
matches!(self, Self::Transitive)
}
/// Returns `true` if (only) direct dependencies should be excluded.
pub fn is_direct(self) -> bool {
matches!(self, Self::Direct)
}
}

View File

@ -1,3 +1,4 @@
pub use dependency_mode::DependencyMode;
pub use error::ResolveError;
pub use finder::{DistFinder, Reporter as FinderReporter};
pub use manifest::Manifest;
@ -10,6 +11,7 @@ pub use resolver::{
};
mod candidate_selector;
mod dependency_mode;
mod error;
mod finder;
mod manifest;

View File

@ -1,12 +1,13 @@
use chrono::{DateTime, Utc};
use crate::{PreReleaseMode, ResolutionMode};
use crate::{DependencyMode, PreReleaseMode, ResolutionMode};
/// Options for resolving a manifest.
#[derive(Debug, Default, Copy, Clone)]
pub struct Options {
pub resolution_mode: ResolutionMode,
pub prerelease_mode: PreReleaseMode,
pub dependency_mode: DependencyMode,
pub exclude_newer: Option<DateTime<Utc>>,
}
@ -15,6 +16,7 @@ pub struct Options {
pub struct OptionsBuilder {
resolution_mode: ResolutionMode,
prerelease_mode: PreReleaseMode,
dependency_mode: DependencyMode,
exclude_newer: Option<DateTime<Utc>>,
}
@ -38,6 +40,13 @@ impl OptionsBuilder {
self
}
/// Sets the dependency mode.
#[must_use]
pub fn dependency_mode(mut self, dependency_mode: DependencyMode) -> Self {
self.dependency_mode = dependency_mode;
self
}
/// Sets the exclusion date.
#[must_use]
pub fn exclude_newer(mut self, exclude_newer: Option<DateTime<Utc>>) -> Self {
@ -50,6 +59,7 @@ impl OptionsBuilder {
Options {
resolution_mode: self.resolution_mode,
prerelease_mode: self.prerelease_mode,
dependency_mode: self.dependency_mode,
exclude_newer: self.exclude_newer,
}
}

View File

@ -50,7 +50,7 @@ pub use crate::resolver::provider::ResolverProvider;
use crate::resolver::reporter::Facade;
pub use crate::resolver::reporter::{BuildId, Reporter};
use crate::version_map::VersionMap;
use crate::Options;
use crate::{DependencyMode, Options};
mod allowed_urls;
mod index;
@ -63,6 +63,7 @@ pub struct Resolver<'a, Provider: ResolverProvider> {
constraints: Vec<Requirement>,
overrides: Overrides,
allowed_urls: AllowedUrls,
dependency_mode: DependencyMode,
markers: &'a MarkerEnvironment,
python_requirement: PythonRequirement,
selector: CandidateSelector,
@ -173,6 +174,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
visited: DashSet::default(),
selector,
allowed_urls,
dependency_mode: options.dependency_mode,
project: manifest.project,
requirements: manifest.requirements,
constraints: manifest.constraints,
@ -644,6 +646,11 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
}
PubGrubPackage::Package(package_name, extra, url) => {
// If we're excluding transitive dependencies, short-circuit.
if self.dependency_mode.is_direct() {
return Ok(Dependencies::Available(DependencyConstraints::default()));
}
// Wait for the metadata to be available.
let dist = match url {
Some(url) => PubGrubDistribution::from_url(package_name, url),

View File

@ -26,8 +26,8 @@ use puffin_installer::{
use puffin_interpreter::{Interpreter, Virtualenv};
use puffin_normalize::PackageName;
use puffin_resolver::{
InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, ResolutionGraph,
ResolutionMode, Resolver,
DependencyMode, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode,
ResolutionGraph, ResolutionMode, Resolver,
};
use puffin_traits::{InFlight, SetupPyStrategy};
use requirements_txt::EditableRequirement;
@ -46,6 +46,7 @@ pub(crate) async fn pip_install(
extras: &ExtrasSpecification<'_>,
resolution_mode: ResolutionMode,
prerelease_mode: PreReleaseMode,
dependency_mode: DependencyMode,
index_locations: IndexLocations,
reinstall: &Reinstall,
link_mode: LinkMode,
@ -154,6 +155,7 @@ pub(crate) async fn pip_install(
let options = OptionsBuilder::new()
.resolution_mode(resolution_mode)
.prerelease_mode(prerelease_mode)
.dependency_mode(dependency_mode)
.exclude_newer(exclude_newer)
.build();

View File

@ -15,7 +15,7 @@ use puffin_cache::{Cache, CacheArgs, Refresh};
use puffin_installer::{NoBinary, Reinstall};
use puffin_interpreter::PythonVersion;
use puffin_normalize::{ExtraName, PackageName};
use puffin_resolver::{PreReleaseMode, ResolutionMode};
use puffin_resolver::{DependencyMode, PreReleaseMode, ResolutionMode};
use puffin_traits::SetupPyStrategy;
use requirements::ExtrasSpecification;
@ -445,6 +445,11 @@ struct PipInstallArgs {
#[clap(long)]
refresh_package: Vec<PackageName>,
/// Ignore package dependencies, instead only installing those packages explicitly listed
/// on the command line or in the requirements files.
#[clap(long)]
no_deps: bool,
/// The method to use when installing packages from the global cache.
#[clap(long, value_enum, default_value_t = install_wheel_rs::linker::LinkMode::default())]
link_mode: install_wheel_rs::linker::LinkMode,
@ -802,6 +807,11 @@ async fn run() -> Result<ExitStatus> {
};
let reinstall = Reinstall::from_args(args.reinstall, args.reinstall_package);
let no_binary = NoBinary::from_args(args.no_binary, args.no_binary_package);
let dependency_mode = if args.no_deps {
DependencyMode::Direct
} else {
DependencyMode::Transitive
};
commands::pip_install(
&requirements,
&constraints,
@ -809,6 +819,7 @@ async fn run() -> Result<ExitStatus> {
&extras,
args.resolution,
args.prerelease,
dependency_mode,
index_urls,
&reinstall,
args.link_mode,

View File

@ -1099,3 +1099,48 @@ fn install_executable_hardlink() -> Result<()> {
Ok(())
}
/// Install a package from the command line into a virtual environment, ignoring its dependencies.
#[test]
fn no_deps() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let cache_dir = assert_fs::TempDir::new()?;
let venv = create_venv_py312(&temp_dir, &cache_dir);
// Install Flask.
insta::with_settings!({
filters => INSTA_FILTERS.to_vec()
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip")
.arg("install")
.arg("Flask")
.arg("--no-deps")
.arg("--strict")
.arg("--cache-dir")
.arg(cache_dir.path())
.arg("--exclude-newer")
.arg(EXCLUDE_NEWER)
.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]
+ flask==3.0.0
warning: The package `flask` requires `werkzeug >=3.0.0`, but it's not installed.
warning: The package `flask` requires `jinja2 >=3.1.2`, but it's not installed.
warning: The package `flask` requires `itsdangerous >=2.1.2`, but it's not installed.
warning: The package `flask` requires `click >=8.1.3`, but it's not installed.
warning: The package `flask` requires `blinker >=1.6.2`, but it's not installed.
"###);
});
assert_command(&venv, "import flask", &temp_dir).failure();
Ok(())
}