Skip to content

Commit

Permalink
Add support for locking and installing scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Dec 24, 2024
1 parent b616818 commit f775a80
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 29 deletions.
7 changes: 7 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3089,6 +3089,13 @@ pub struct LockArgs {
#[arg(long, conflicts_with = "check_exists", conflicts_with = "check")]
pub dry_run: bool,

/// Lock the specified Python script, rather than the current project.
///
/// If provided, uv will lock the script based on its inline metadata table, in adherence
/// with PEP 723.
#[arg(long)]
pub script: Option<PathBuf>,

#[command(flatten)]
pub resolver: ResolverArgs,

Expand Down
76 changes: 51 additions & 25 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,6 @@ use owo_colors::OwoColorize;
use rustc_hash::{FxBuildHasher, FxHashMap};
use tracing::debug;

use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, SummaryResolveLogger};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{ProjectError, ProjectInterpreter};
use crate::commands::reporters::ResolverReporter;
use crate::commands::{diagnostics, pip, ExitStatus};
use crate::printer::Printer;
use crate::settings::{ResolverSettings, ResolverSettingsRef};
use uv_cache::Cache;
use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Expand All @@ -38,11 +31,20 @@ use uv_resolver::{
FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, PythonRequirement, RequiresPython,
ResolverEnvironment, ResolverManifest, SatisfiesResult, UniversalMarker,
};
use uv_scripts::{Pep723Item, Pep723Script};
use uv_settings::PythonInstallMirrors;
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceMember};

use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, SummaryResolveLogger};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{ProjectError, ProjectInterpreter, ScriptInterpreter};
use crate::commands::reporters::ResolverReporter;
use crate::commands::{diagnostics, pip, ExitStatus};
use crate::printer::Printer;
use crate::settings::{ResolverSettings, ResolverSettingsRef};

/// The result of running a lock operation.
#[derive(Debug, Clone)]
pub(crate) enum LockResult {
Expand Down Expand Up @@ -78,6 +80,7 @@ pub(crate) async fn lock(
python: Option<String>,
install_mirrors: PythonInstallMirrors,
settings: ResolverSettings,
script: Option<Pep723Script>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
connectivity: Connectivity,
Expand All @@ -90,29 +93,52 @@ pub(crate) async fn lock(
preview: PreviewMode,
) -> anyhow::Result<ExitStatus> {
// Find the project requirements.
let workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?;
let workspace;
let target = if let Some(script) = script.as_ref() {
LockTarget::Script(script)
} else {
workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?;
LockTarget::Workspace(&workspace)
};

// Determine the lock mode.
let interpreter;
let mode = if frozen {
LockMode::Frozen
} else {
interpreter = ProjectInterpreter::discover(
&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();
interpreter = match target {
LockTarget::Workspace(workspace) => ProjectInterpreter::discover(
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(),
LockTarget::Script(script) => 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(),
};

if locked {
LockMode::Locked(&interpreter)
Expand All @@ -129,7 +155,7 @@ pub(crate) async fn lock(
// Perform the lock operation.
match do_safe_lock(
mode,
(&workspace).into(),
target,
settings.as_ref(),
LowerBound::Warn,
&state,
Expand Down
113 changes: 111 additions & 2 deletions crates/uv/src/commands/project/lock_target.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use itertools::Either;

use uv_configuration::{LowerBound, SourceStrategy};
use uv_distribution::LoweredRequirement;
use uv_distribution_types::IndexLocations;
use uv_normalize::PackageName;
use uv_pep508::RequirementOrigin;
use uv_pypi_types::{Conflicts, Requirement, SupportedEnvironments, VerbatimParsedUrl};
use uv_resolver::{Lock, LockVersion, RequiresPython, VERSION};
use uv_scripts::Pep723Script;
use uv_workspace::dependency_groups::DependencyGroupError;
use uv_workspace::{Workspace, WorkspaceMember};

Expand All @@ -16,6 +20,7 @@ use crate::commands::project::{find_requires_python, ProjectError};
#[derive(Debug, Copy, Clone)]
pub(crate) enum LockTarget<'lock> {
Workspace(&'lock Workspace),
Script(&'lock Pep723Script),
}

impl<'lock> From<&'lock Workspace> for LockTarget<'lock> {
Expand All @@ -24,6 +29,12 @@ impl<'lock> From<&'lock Workspace> for LockTarget<'lock> {
}
}

impl<'lock> From<&'lock Pep723Script> for LockTarget<'lock> {
fn from(script: &'lock Pep723Script) -> Self {
LockTarget::Script(script)
}
}

impl<'lock> LockTarget<'lock> {
/// Returns any requirements that are exclusive to the workspace root, i.e., not included in
/// any of the workspace members.
Expand All @@ -32,20 +43,41 @@ impl<'lock> LockTarget<'lock> {
) -> Result<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>, DependencyGroupError> {
match self {
Self::Workspace(workspace) => workspace.non_project_requirements(),
Self::Script(script) => Ok(script.metadata.dependencies.clone().unwrap_or_default()),
}
}

/// Returns the set of overrides for the [`LockTarget`].
pub(crate) fn overrides(self) -> Vec<uv_pep508::Requirement<VerbatimParsedUrl>> {
match self {
Self::Workspace(workspace) => workspace.overrides(),
Self::Script(script) => script
.metadata
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.override_dependencies.as_ref())
.into_iter()
.flatten()
.cloned()
.collect(),
}
}

/// Returns the set of constraints for the [`LockTarget`].
pub(crate) fn constraints(self) -> Vec<uv_pep508::Requirement<VerbatimParsedUrl>> {
match self {
Self::Workspace(workspace) => workspace.constraints(),
Self::Script(script) => script
.metadata
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.constraint_dependencies.as_ref())
.into_iter()
.flatten()
.cloned()
.collect(),
}
}

Expand Down Expand Up @@ -83,6 +115,55 @@ impl<'lock> LockTarget<'lock> {
.map(|requirement| requirement.with_origin(RequirementOrigin::Workspace))
.collect::<Vec<_>>())
}
Self::Script(script) => {
// Collect any `tool.uv.index` from the script.
let empty = Vec::default();
let indexes = match sources {
SourceStrategy::Enabled => script
.metadata
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.top_level.index.as_deref())
.unwrap_or(&empty),
SourceStrategy::Disabled => &empty,
};

// Collect any `tool.uv.sources` from the script.
let empty = BTreeMap::default();
let sources = match sources {
SourceStrategy::Enabled => script
.metadata
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.sources.as_ref())
.unwrap_or(&empty),
SourceStrategy::Disabled => &empty,
};

Ok(requirements
.into_iter()
.flat_map(|requirement| {
let requirement_name = requirement.name.clone();
LoweredRequirement::from_non_workspace_requirement(
requirement,
script.path.parent().unwrap(),
sources,
indexes,
locations,
LowerBound::Allow,
)
.map(move |requirement| match requirement {
Ok(requirement) => Ok(requirement.into_inner()),
Err(err) => Err(uv_distribution::MetadataError::LoweringError(
requirement_name.clone(),
Box::new(err),
)),
})
})
.collect::<Result<_, _>>()?)
}
}
}

Expand All @@ -102,62 +183,90 @@ impl<'lock> LockTarget<'lock> {

members
}
Self::Script(_) => Vec::new(),
}
}

/// Return the list of packages.
pub(crate) fn packages(self) -> &'lock BTreeMap<PackageName, WorkspaceMember> {
match self {
Self::Workspace(workspace) => workspace.packages(),
Self::Script(_) => {
static EMPTY: BTreeMap<PackageName, WorkspaceMember> = BTreeMap::new();
&EMPTY
}
}
}

/// Returns the set of supported environments for the [`LockTarget`].
pub(crate) fn environments(self) -> Option<&'lock SupportedEnvironments> {
match self {
Self::Workspace(workspace) => workspace.environments(),
Self::Script(_) => {
// TODO(charlie): Add support for environments in scripts.
None
}
}
}

/// Returns the set of conflicts for the [`LockTarget`].
pub(crate) fn conflicts(self) -> Conflicts {
match self {
Self::Workspace(workspace) => workspace.conflicts(),
Self::Script(_) => Conflicts::empty(),
}
}

/// Return the `Requires-Python` bound for the [`LockTarget`].
pub(crate) fn requires_python(self) -> Option<RequiresPython> {
match self {
Self::Workspace(workspace) => find_requires_python(workspace),
Self::Script(script) => script
.metadata
.requires_python
.as_ref()
.map(RequiresPython::from_specifiers),
}
}

/// Returns the set of requirements that include all packages in the workspace.
pub(crate) fn members_requirements(self) -> impl Iterator<Item = Requirement> + 'lock {
match self {
Self::Workspace(workspace) => workspace.members_requirements(),
Self::Workspace(workspace) => Either::Left(workspace.members_requirements()),
Self::Script(_) => Either::Right(std::iter::empty()),
}
}

/// Returns the set of requirements that include all packages in the workspace.
pub(crate) fn group_requirements(self) -> impl Iterator<Item = Requirement> + 'lock {
match self {
Self::Workspace(workspace) => workspace.group_requirements(),
Self::Workspace(workspace) => Either::Left(workspace.group_requirements()),
Self::Script(_) => Either::Right(std::iter::empty()),
}
}

/// Return the path to the lock root.
pub(crate) fn install_path(self) -> &'lock Path {
match self {
Self::Workspace(workspace) => workspace.install_path(),
Self::Script(script) => script.path.parent().unwrap(),
}
}

/// Return the path to the lockfile.
fn lock_path(self) -> PathBuf {
match self {
// `uv.lock`
Self::Workspace(workspace) => workspace.install_path().join("uv.lock"),
// `script.py.lock`
Self::Script(script) => {
let mut file_name = match script.path.file_name() {
Some(f) => f.to_os_string(),
None => panic!("Script path has no file name"),
};
file_name.push(".lock");
script.path.with_file_name(file_name)
}
}
}

Expand Down
Loading

0 comments on commit f775a80

Please sign in to comment.