Allow uv pip sync to clear an environment with opt-in (#4517)

Closes https://github.com/astral-sh/uv/issues/4516

Open to some deliberation about the opt-in strategy here.
This commit is contained in:
Zanie Blue 2024-07-02 09:14:27 -04:00 committed by GitHub
parent d9f389a58d
commit 2c0cb6e021
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 116 additions and 7 deletions

View file

@ -826,6 +826,13 @@ pub struct PipSyncArgs {
#[arg(long, conflicts_with = "no_build")]
pub only_binary: Option<Vec<PackageNameSpecifier>>,
/// Allow sync of empty requirements, which will clear the environment of all packages.
#[arg(long, overrides_with("no_allow_empty_requirements"))]
pub allow_empty_requirements: bool,
#[arg(long, overrides_with("allow_empty_requirements"))]
pub no_allow_empty_requirements: bool,
/// The minimum Python version that should be supported by the requirements (e.g.,
/// `3.7` or `3.7.9`).
///

View file

@ -165,6 +165,7 @@ pub struct PipOptions {
pub extra: Option<Vec<ExtraName>>,
pub all_extras: Option<bool>,
pub no_deps: Option<bool>,
pub allow_empty_requirements: Option<bool>,
pub resolution: Option<ResolutionMode>,
pub prerelease: Option<PreReleaseMode>,
pub output_file: Option<PathBuf>,

View file

@ -47,6 +47,7 @@ pub(crate) async fn pip_sync(
index_strategy: IndexStrategy,
keyring_provider: KeyringProviderType,
setup_py: SetupPyStrategy,
allow_empty_requirements: bool,
connectivity: Connectivity,
config_settings: &ConfigSettings,
no_build_isolation: bool,
@ -104,10 +105,12 @@ pub(crate) async fn pip_sync(
.await?;
// Validate that the requirements are non-empty.
let num_requirements = requirements.len() + source_trees.len();
if num_requirements == 0 {
writeln!(printer.stderr(), "No requirements found")?;
return Ok(ExitStatus::Success);
if !allow_empty_requirements {
let num_requirements = requirements.len() + source_trees.len();
if num_requirements == 0 {
writeln!(printer.stderr(), "No requirements found (hint: use `--allow-empty-requirements` to clear the environment)")?;
return Ok(ExitStatus::Success);
}
}
// Detect the current Python interpreter.

View file

@ -337,6 +337,7 @@ async fn run() -> Result<ExitStatus> {
args.settings.index_strategy,
args.settings.keyring_provider,
args.settings.setup_py,
args.settings.allow_empty_requirements,
globals.connectivity,
&args.settings.config_setting,
args.settings.no_build_isolation,

View file

@ -759,6 +759,8 @@ impl PipSyncSettings {
no_break_system_packages,
target,
prefix,
allow_empty_requirements,
no_allow_empty_requirements,
legacy_setup_py,
no_legacy_setup_py,
no_build_isolation,
@ -791,15 +793,19 @@ impl PipSyncSettings {
exclude_newer,
target,
prefix,
require_hashes: flag(require_hashes, no_require_hashes),
no_build: flag(no_build, build),
no_binary,
only_binary,
no_build_isolation: flag(no_build_isolation, build_isolation),
strict: flag(strict, no_strict),
allow_empty_requirements: flag(
allow_empty_requirements,
no_allow_empty_requirements,
),
legacy_setup_py: flag(legacy_setup_py, no_legacy_setup_py),
no_build_isolation: flag(no_build_isolation, build_isolation),
python_version,
python_platform,
require_hashes: flag(require_hashes, no_require_hashes),
strict: flag(strict, no_strict),
concurrent_builds: env(env::CONCURRENT_BUILDS),
concurrent_downloads: env(env::CONCURRENT_DOWNLOADS),
concurrent_installs: env(env::CONCURRENT_INSTALLS),
@ -1629,6 +1635,7 @@ pub(crate) struct PipSettings {
pub(crate) keyring_provider: KeyringProviderType,
pub(crate) no_build_isolation: bool,
pub(crate) build_options: BuildOptions,
pub(crate) allow_empty_requirements: bool,
pub(crate) strict: bool,
pub(crate) dependency_mode: DependencyMode,
pub(crate) resolution: ResolutionMode,
@ -1688,6 +1695,7 @@ impl PipSettings {
extra,
all_extras,
no_deps,
allow_empty_requirements,
resolution,
prerelease,
output_file,
@ -1814,6 +1822,10 @@ impl PipSettings {
.generate_hashes
.combine(generate_hashes)
.unwrap_or_default(),
allow_empty_requirements: args
.allow_empty_requirements
.combine(allow_empty_requirements)
.unwrap_or_default(),
setup_py: if args
.legacy_setup_py
.combine(legacy_setup_py)

View file

@ -250,6 +250,68 @@ fn noop() -> Result<()> {
Ok(())
}
/// Attempt to sync an empty set of requirements.
#[test]
fn pip_sync_empty() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.touch()?;
uv_snapshot!(context.pip_sync()
.arg("requirements.txt"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: Requirements file requirements.txt does not contain any dependencies
No requirements found (hint: use `--allow-empty-requirements` to clear the environment)
"###
);
uv_snapshot!(context.pip_sync()
.arg("requirements.txt")
.arg("--allow-empty-requirements"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: Requirements file requirements.txt does not contain any dependencies
Resolved 0 packages in [TIME]
Audited 0 packages in [TIME]
"###
);
// Install a package.
requirements_txt.write_str("iniconfig==2.0.0")?;
context
.pip_sync()
.arg("requirements.txt")
.assert()
.success();
// Now, syncing should remove the package.
requirements_txt.write_str("")?;
uv_snapshot!(context.pip_sync()
.arg("requirements.txt")
.arg("--allow-empty-requirements"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: Requirements file requirements.txt does not contain any dependencies
Resolved 0 packages in [TIME]
Uninstalled 1 package in [TIME]
- iniconfig==2.0.0
"###
);
Ok(())
}
/// Install a package into a virtual environment, then install the same package into a different
/// virtual environment.
#[test]

View file

@ -124,6 +124,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: LowestDirect,
@ -255,6 +256,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: Highest,
@ -387,6 +389,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: Highest,
@ -551,6 +554,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: LowestDirect,
@ -661,6 +665,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: Highest,
@ -803,6 +808,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: LowestDirect,
@ -982,6 +988,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: Highest,
@ -1160,6 +1167,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: Highest,
@ -1311,6 +1319,7 @@ fn resolve_find_links() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: Highest,
@ -1443,6 +1452,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: LowestDirect,
@ -1613,6 +1623,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: Highest,
@ -1766,6 +1777,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: LowestDirect,
@ -1898,6 +1910,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: LowestDirect,
@ -2013,6 +2026,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: LowestDirect,
@ -2128,6 +2142,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: Highest,
@ -2245,6 +2260,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: LowestDirect,
@ -2387,6 +2403,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: LowestDirect,

6
uv.schema.json generated
View file

@ -449,6 +449,12 @@
"null"
]
},
"allow-empty-requirements": {
"type": [
"boolean",
"null"
]
},
"annotation-style": {
"anyOf": [
{