Skip to content

Commit

Permalink
Add remove support for PEP 723 scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Dec 25, 2024
1 parent 1a6d916 commit 15ef364
Show file tree
Hide file tree
Showing 3 changed files with 430 additions and 76 deletions.
40 changes: 20 additions & 20 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -992,9 +992,27 @@ fn resolve_requirement(
Ok((processed_requirement, source))
}

/// A Python [`Interpreter`] or [`PythonEnvironment`] for a project.
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub(super) enum PythonTarget {
Interpreter(Interpreter),
Environment(PythonEnvironment),
}

impl PythonTarget {
/// Return the [`Interpreter`] for the project.
fn interpreter(&self) -> &Interpreter {
match self {
Self::Interpreter(interpreter) => interpreter,
Self::Environment(venv) => venv.interpreter(),
}
}
}

/// Represents the destination where dependencies are added, either to a project or a script.
#[derive(Debug, Clone)]
enum AddTarget {
pub(super) enum AddTarget {
/// A PEP 723 script, with inline metadata.
Script(Pep723Script, Box<Interpreter>),

Expand All @@ -1013,7 +1031,7 @@ impl<'lock> From<&'lock AddTarget> for LockTarget<'lock> {

impl AddTarget {
/// Returns the [`Interpreter`] for the target.
fn interpreter(&self) -> &Interpreter {
pub(super) fn interpreter(&self) -> &Interpreter {
match self {
Self::Script(_, interpreter) => interpreter,
Self::Project(_, venv) => venv.interpreter(),
Expand Down Expand Up @@ -1133,24 +1151,6 @@ impl AddTargetSnapshot {
}
}

/// A Python [`Interpreter`] or [`PythonEnvironment`] for a project.
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
enum PythonTarget {
Interpreter(Interpreter),
Environment(PythonEnvironment),
}

impl PythonTarget {
/// Return the [`Interpreter`] for the project.
fn interpreter(&self) -> &Interpreter {
match self {
Self::Interpreter(interpreter) => interpreter,
Self::Environment(venv) => venv.interpreter(),
}
}
}

#[derive(Debug, Clone)]
struct DependencyEdit {
dependency_type: DependencyType,
Expand Down
204 changes: 153 additions & 51 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use std::fmt::Write;
use std::path::Path;

use anyhow::{Context, Result};
use owo_colors::OwoColorize;

use std::fmt::Write;
use std::io;
use std::path::Path;
use std::str::FromStr;
use tracing::debug;
use uv_cache::Cache;
use uv_client::Connectivity;
use uv_configuration::{
Expand All @@ -15,7 +16,7 @@ use uv_fs::Simplified;
use uv_normalize::DEV_DEPENDENCIES;
use uv_pep508::PackageName;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
use uv_scripts::Pep723Script;
use uv_scripts::{Pep723Item, Pep723Metadata, Pep723Script};
use uv_settings::PythonInstallMirrors;
use uv_warnings::warn_user_once;
use uv_workspace::pyproject::DependencyType;
Expand All @@ -24,9 +25,13 @@ use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace};

use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
use crate::commands::pip::operations::Modifications;
use crate::commands::project::add::{AddTarget, PythonTarget};
use crate::commands::project::install_target::InstallTarget;
use crate::commands::project::lock::LockMode;
use crate::commands::project::{default_dependency_groups, ProjectError};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
default_dependency_groups, ProjectError, ProjectInterpreter, ScriptInterpreter,
};
use crate::commands::{diagnostics, project, ExitStatus};
use crate::printer::Printer;
use crate::settings::ResolverInstallerSettings;
Expand Down Expand Up @@ -79,7 +84,7 @@ pub(crate) async fn remove(
"`--no-sync` is a no-op for Python scripts with inline metadata, which always run in isolation"
);
}
Target::Script(script)
RemoveTarget::Script(script)
} else {
// Find the project in the workspace.
let project = if let Some(package) = package {
Expand All @@ -93,14 +98,14 @@ pub(crate) async fn remove(
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
};

Target::Project(project)
RemoveTarget::Project(project)
};

let mut toml = match &target {
Target::Script(script) => {
RemoveTarget::Script(script) => {
PyProjectTomlMut::from_toml(&script.metadata.raw, DependencyTarget::Script)
}
Target::Project(project) => PyProjectTomlMut::from_toml(
RemoveTarget::Project(project) => PyProjectTomlMut::from_toml(
project.pyproject_toml().raw.as_ref(),
DependencyTarget::PyProjectToml,
),
Expand Down Expand Up @@ -161,59 +166,101 @@ pub(crate) async fn remove(
}
}

// Save the modified dependencies.
match &target {
Target::Script(script) => {
script.write(&toml.to_string())?;
}
Target::Project(project) => {
let pyproject_path = project.root().join("pyproject.toml");
fs_err::write(pyproject_path, toml.to_string())?;
}
};
let content = toml.to_string();

// If `--frozen`, exit early. There's no reason to lock and sync, and we don't need a `uv.lock`
// Save the modified `pyproject.toml` or script.
target.write(&content)?;

// If `--frozen`, exit early. There's no reason to lock and sync, since we don't need a `uv.lock`
// to exist at all.
if frozen {
return Ok(ExitStatus::Success);
}

let project = match target {
Target::Project(project) => project,
// If `--script`, exit early. There's no reason to lock and sync.
Target::Script(script) => {
// If we're modifying a script, and lockfile doesn't exist, don't create it.
if let RemoveTarget::Script(ref script) = target {
if !LockTarget::from(script).lock_path().is_file() {
writeln!(
printer.stderr(),
"Updated `{}`",
script.path.user_display().cyan()
)?;
return Ok(ExitStatus::Success);
}
};
}

// Discover or create the virtual environment.
let venv = project::get_or_init_environment(
project.workspace(),
python.as_deref().map(PythonRequest::parse),
&install_mirrors,
python_preference,
python_downloads,
connectivity,
native_tls,
allow_insecure_host,
no_config,
cache,
printer,
)
.await?;
// Update the `pypackage.toml` in-memory.
let target = target.update(&content)?;

// Convert to an `AddTarget` by attaching the appropriate interpreter or environment.
let target = match target {
RemoveTarget::Project(project) => {
if no_sync {
// Discover the interpreter.
let interpreter = ProjectInterpreter::discover(
project.workspace(),
project_dir,
python.as_deref().map(PythonRequest::parse),
python_preference,
python_downloads,
connectivity,
native_tls,
allow_insecure_host,
&install_mirrors,
no_config,
cache,
printer,
)
.await?
.into_interpreter();

AddTarget::Project(project, Box::new(PythonTarget::Interpreter(interpreter)))
} else {
// Discover or create the virtual environment.
let venv = project::get_or_init_environment(
project.workspace(),
python.as_deref().map(PythonRequest::parse),
&install_mirrors,
python_preference,
python_downloads,
connectivity,
native_tls,
allow_insecure_host,
no_config,
cache,
printer,
)
.await?;

AddTarget::Project(project, Box::new(PythonTarget::Environment(venv)))
}
}
RemoveTarget::Script(script) => {
let interpreter = ScriptInterpreter::discover(
&Pep723Item::Script(script.clone()),
python.as_deref().map(PythonRequest::parse),
python_preference,
python_downloads,
connectivity,
native_tls,
allow_insecure_host,
&install_mirrors,
no_config,
cache,
printer,
)
.await?
.into_interpreter();

AddTarget::Script(script, Box::new(interpreter))
}
};

// Determine the lock mode.
let mode = if frozen {
LockMode::Frozen
} else if locked {
LockMode::Locked(venv.interpreter())
let mode = if locked {
LockMode::Locked(target.interpreter())
} else {
LockMode::Write(venv.interpreter())
LockMode::Write(target.interpreter())
};

// Initialize any shared state.
Expand All @@ -222,7 +269,7 @@ pub(crate) async fn remove(
// Lock and sync the environment, if necessary.
let lock = match project::lock::do_safe_lock(
mode,
project.workspace().into(),
(&target).into(),
settings.as_ref().into(),
LowerBound::Allow,
&state,
Expand All @@ -246,9 +293,15 @@ pub(crate) async fn remove(
Err(err) => return Err(err.into()),
};

if no_sync {
let AddTarget::Project(project, environment) = target else {
// If we're not adding to a project, exit early.
return Ok(ExitStatus::Success);
}
};

let PythonTarget::Environment(venv) = &*environment else {
// If we're not syncing, exit early.
return Ok(ExitStatus::Success);
};

// Perform a full sync, because we don't know what exactly is affected by the removal.
// TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here?
Expand All @@ -273,7 +326,7 @@ pub(crate) async fn remove(

match project::sync::do_sync(
target,
&venv,
venv,
&extras,
&DevGroupsManifest::from_defaults(defaults),
EditableMode::Editable,
Expand Down Expand Up @@ -306,13 +359,62 @@ pub(crate) async fn remove(

/// Represents the destination where dependencies are added, either to a project or a script.
#[derive(Debug)]
enum Target {
enum RemoveTarget {
/// A PEP 723 script, with inline metadata.
Project(VirtualProject),
/// A project with a `pyproject.toml`.
Script(Pep723Script),
}

impl RemoveTarget {
/// Write the updated content to the target.
///
/// Returns `true` if the content was modified.
fn write(&self, content: &str) -> Result<bool, io::Error> {
match self {
Self::Script(script) => {
if content == script.metadata.raw {
debug!("No changes to dependencies; skipping update");
Ok(false)
} else {
script.write(content)?;
Ok(true)
}
}
Self::Project(project) => {
if content == project.pyproject_toml().raw {
debug!("No changes to dependencies; skipping update");
Ok(false)
} else {
let pyproject_path = project.root().join("pyproject.toml");
fs_err::write(pyproject_path, content)?;
Ok(true)
}
}
}
}

/// Update the target in-memory to incorporate the new content.
#[allow(clippy::result_large_err)]
fn update(self, content: &str) -> Result<Self, ProjectError> {
match self {
Self::Script(mut script) => {
script.metadata = Pep723Metadata::from_str(content)
.map_err(ProjectError::Pep723ScriptTomlParse)?;
Ok(Self::Script(script))
}
Self::Project(project) => {
let project = project
.with_pyproject_toml(
toml::from_str(content).map_err(ProjectError::PyprojectTomlParse)?,
)
.ok_or(ProjectError::PyprojectTomlUpdate)?;
Ok(Self::Project(project))
}
}
}
}

/// Show a hint if a dependency with the given name is present as any dependency type.
///
/// This is useful when a dependency of the user-specified type was not found, but it may be present
Expand Down
Loading

0 comments on commit 15ef364

Please sign in to comment.