Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a9c8dcb

Browse files
committedDec 25, 2024·
Add support for locking and installing scripts
1 parent f872b56 commit a9c8dcb

File tree

6 files changed

+186
-23
lines changed

6 files changed

+186
-23
lines changed
 

‎crates/uv-cli/src/lib.rs

+7
Original file line numberDiff line numberDiff line change
@@ -3089,6 +3089,13 @@ pub struct LockArgs {
30893089
#[arg(long, conflicts_with = "check_exists", conflicts_with = "check")]
30903090
pub dry_run: bool,
30913091

3092+
/// Lock the specified Python script, rather than the current project.
3093+
///
3094+
/// If provided, uv will lock the script based on its inline metadata table, in adherence
3095+
/// with PEP 723.
3096+
#[arg(long)]
3097+
pub script: Option<PathBuf>,
3098+
30923099
#[command(flatten)]
30933100
pub resolver: ResolverArgs,
30943101

‎crates/uv/src/commands/project/lock.rs

+44-19
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,15 @@ use uv_resolver::{
3131
FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, PythonRequirement, RequiresPython,
3232
ResolverEnvironment, ResolverManifest, SatisfiesResult, UniversalMarker,
3333
};
34+
use uv_scripts::{Pep723Item, Pep723Script};
3435
use uv_settings::PythonInstallMirrors;
3536
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy};
3637
use uv_warnings::{warn_user, warn_user_once};
3738
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceMember};
3839

3940
use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, SummaryResolveLogger};
4041
use crate::commands::project::lock_target::LockTarget;
41-
use crate::commands::project::{ProjectError, ProjectInterpreter};
42+
use crate::commands::project::{ProjectError, ProjectInterpreter, ScriptInterpreter};
4243
use crate::commands::reporters::ResolverReporter;
4344
use crate::commands::{diagnostics, pip, ExitStatus};
4445
use crate::printer::Printer;
@@ -79,6 +80,7 @@ pub(crate) async fn lock(
7980
python: Option<String>,
8081
install_mirrors: PythonInstallMirrors,
8182
settings: ResolverSettings,
83+
script: Option<Pep723Script>,
8284
python_preference: PythonPreference,
8385
python_downloads: PythonDownloads,
8486
connectivity: Connectivity,
@@ -91,29 +93,52 @@ pub(crate) async fn lock(
9193
preview: PreviewMode,
9294
) -> anyhow::Result<ExitStatus> {
9395
// Find the project requirements.
94-
let workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?;
96+
let workspace;
97+
let target = if let Some(script) = script.as_ref() {
98+
LockTarget::Script(script)
99+
} else {
100+
workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?;
101+
LockTarget::Workspace(&workspace)
102+
};
95103

96104
// Determine the lock mode.
97105
let interpreter;
98106
let mode = if frozen {
99107
LockMode::Frozen
100108
} else {
101-
interpreter = ProjectInterpreter::discover(
102-
&workspace,
103-
project_dir,
104-
python.as_deref().map(PythonRequest::parse),
105-
python_preference,
106-
python_downloads,
107-
connectivity,
108-
native_tls,
109-
allow_insecure_host,
110-
&install_mirrors,
111-
no_config,
112-
cache,
113-
printer,
114-
)
115-
.await?
116-
.into_interpreter();
109+
interpreter = match target {
110+
LockTarget::Workspace(workspace) => ProjectInterpreter::discover(
111+
workspace,
112+
project_dir,
113+
python.as_deref().map(PythonRequest::parse),
114+
python_preference,
115+
python_downloads,
116+
connectivity,
117+
native_tls,
118+
allow_insecure_host,
119+
&install_mirrors,
120+
no_config,
121+
cache,
122+
printer,
123+
)
124+
.await?
125+
.into_interpreter(),
126+
LockTarget::Script(script) => ScriptInterpreter::discover(
127+
&Pep723Item::Script(script.clone()),
128+
python.as_deref().map(PythonRequest::parse),
129+
python_preference,
130+
python_downloads,
131+
connectivity,
132+
native_tls,
133+
allow_insecure_host,
134+
&install_mirrors,
135+
no_config,
136+
cache,
137+
printer,
138+
)
139+
.await?
140+
.into_interpreter(),
141+
};
117142

118143
if locked {
119144
LockMode::Locked(&interpreter)
@@ -130,7 +155,7 @@ pub(crate) async fn lock(
130155
// Perform the lock operation.
131156
match do_safe_lock(
132157
mode,
133-
(&workspace).into(),
158+
target,
134159
settings.as_ref(),
135160
LowerBound::Warn,
136161
&state,

‎crates/uv/src/commands/project/lock_target.rs

+112-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
use std::collections::BTreeMap;
22
use std::path::{Path, PathBuf};
33

4+
use itertools::Either;
5+
46
use uv_configuration::{LowerBound, SourceStrategy};
7+
use uv_distribution::LoweredRequirement;
58
use uv_distribution_types::IndexLocations;
69
use uv_normalize::{GroupName, PackageName};
710
use uv_pep508::RequirementOrigin;
811
use uv_pypi_types::{Conflicts, Requirement, SupportedEnvironments, VerbatimParsedUrl};
912
use uv_resolver::{Lock, LockVersion, RequiresPython, VERSION};
13+
use uv_scripts::Pep723Script;
1014
use uv_workspace::dependency_groups::DependencyGroupError;
1115
use uv_workspace::{Workspace, WorkspaceMember};
1216

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

2126
impl<'lock> From<&'lock Workspace> for LockTarget<'lock> {
@@ -24,26 +29,53 @@ impl<'lock> From<&'lock Workspace> for LockTarget<'lock> {
2429
}
2530
}
2631

32+
impl<'lock> From<&'lock Pep723Script> for LockTarget<'lock> {
33+
fn from(script: &'lock Pep723Script) -> Self {
34+
LockTarget::Script(script)
35+
}
36+
}
37+
2738
impl<'lock> LockTarget<'lock> {
2839
/// Return the set of requirements that are attached to the target directly, as opposed to being
2940
/// attached to any members within the target.
3041
pub(crate) fn requirements(self) -> Vec<uv_pep508::Requirement<VerbatimParsedUrl>> {
3142
match self {
3243
Self::Workspace(workspace) => workspace.requirements(),
44+
Self::Script(script) => script.metadata.dependencies.clone().unwrap_or_default(),
3345
}
3446
}
3547

3648
/// Returns the set of overrides for the [`LockTarget`].
3749
pub(crate) fn overrides(self) -> Vec<uv_pep508::Requirement<VerbatimParsedUrl>> {
3850
match self {
3951
Self::Workspace(workspace) => workspace.overrides(),
52+
Self::Script(script) => script
53+
.metadata
54+
.tool
55+
.as_ref()
56+
.and_then(|tool| tool.uv.as_ref())
57+
.and_then(|uv| uv.override_dependencies.as_ref())
58+
.into_iter()
59+
.flatten()
60+
.cloned()
61+
.collect(),
4062
}
4163
}
4264

4365
/// Returns the set of constraints for the [`LockTarget`].
4466
pub(crate) fn constraints(self) -> Vec<uv_pep508::Requirement<VerbatimParsedUrl>> {
4567
match self {
4668
Self::Workspace(workspace) => workspace.constraints(),
69+
Self::Script(script) => script
70+
.metadata
71+
.tool
72+
.as_ref()
73+
.and_then(|tool| tool.uv.as_ref())
74+
.and_then(|uv| uv.constraint_dependencies.as_ref())
75+
.into_iter()
76+
.flatten()
77+
.cloned()
78+
.collect(),
4779
}
4880
}
4981

@@ -57,20 +89,23 @@ impl<'lock> LockTarget<'lock> {
5789
> {
5890
match self {
5991
Self::Workspace(workspace) => workspace.dependency_groups(),
92+
Self::Script(_) => Ok(BTreeMap::new()),
6093
}
6194
}
6295

6396
/// Returns the set of all members within the target.
6497
pub(crate) fn members_requirements(self) -> impl Iterator<Item = Requirement> + 'lock {
6598
match self {
66-
Self::Workspace(workspace) => workspace.members_requirements(),
99+
Self::Workspace(workspace) => Either::Left(workspace.members_requirements()),
100+
Self::Script(_) => Either::Right(std::iter::empty()),
67101
}
68102
}
69103

70104
/// Returns the set of all dependency groups within the target.
71105
pub(crate) fn group_requirements(self) -> impl Iterator<Item = Requirement> + 'lock {
72106
match self {
73-
Self::Workspace(workspace) => workspace.group_requirements(),
107+
Self::Workspace(workspace) => Either::Left(workspace.group_requirements()),
108+
Self::Script(_) => Either::Right(std::iter::empty()),
74109
}
75110
}
76111

@@ -90,48 +125,74 @@ impl<'lock> LockTarget<'lock> {
90125

91126
members
92127
}
128+
Self::Script(_) => Vec::new(),
93129
}
94130
}
95131

96132
/// Return the list of packages.
97133
pub(crate) fn packages(self) -> &'lock BTreeMap<PackageName, WorkspaceMember> {
98134
match self {
99135
Self::Workspace(workspace) => workspace.packages(),
136+
Self::Script(_) => {
137+
static EMPTY: BTreeMap<PackageName, WorkspaceMember> = BTreeMap::new();
138+
&EMPTY
139+
}
100140
}
101141
}
102142

103143
/// Returns the set of supported environments for the [`LockTarget`].
104144
pub(crate) fn environments(self) -> Option<&'lock SupportedEnvironments> {
105145
match self {
106146
Self::Workspace(workspace) => workspace.environments(),
147+
Self::Script(_) => {
148+
// TODO(charlie): Add support for environments in scripts.
149+
None
150+
}
107151
}
108152
}
109153

110154
/// Returns the set of conflicts for the [`LockTarget`].
111155
pub(crate) fn conflicts(self) -> Conflicts {
112156
match self {
113157
Self::Workspace(workspace) => workspace.conflicts(),
158+
Self::Script(_) => Conflicts::empty(),
114159
}
115160
}
116161

117162
/// Return the `Requires-Python` bound for the [`LockTarget`].
118163
pub(crate) fn requires_python(self) -> Option<RequiresPython> {
119164
match self {
120165
Self::Workspace(workspace) => find_requires_python(workspace),
166+
Self::Script(script) => script
167+
.metadata
168+
.requires_python
169+
.as_ref()
170+
.map(RequiresPython::from_specifiers),
121171
}
122172
}
123173

124174
/// Return the path to the lock root.
125175
pub(crate) fn install_path(self) -> &'lock Path {
126176
match self {
127177
Self::Workspace(workspace) => workspace.install_path(),
178+
Self::Script(script) => script.path.parent().unwrap(),
128179
}
129180
}
130181

131182
/// Return the path to the lockfile.
132183
pub(crate) fn lock_path(self) -> PathBuf {
133184
match self {
185+
// `uv.lock`
134186
Self::Workspace(workspace) => workspace.install_path().join("uv.lock"),
187+
// `script.py.lock`
188+
Self::Script(script) => {
189+
let mut file_name = match script.path.file_name() {
190+
Some(f) => f.to_os_string(),
191+
None => panic!("Script path has no file name"),
192+
};
193+
file_name.push(".lock");
194+
script.path.with_file_name(file_name)
195+
}
135196
}
136197
}
137198

@@ -223,6 +284,55 @@ impl<'lock> LockTarget<'lock> {
223284
.map(|requirement| requirement.with_origin(RequirementOrigin::Workspace))
224285
.collect::<Vec<_>>())
225286
}
287+
Self::Script(script) => {
288+
// Collect any `tool.uv.index` from the script.
289+
let empty = Vec::default();
290+
let indexes = match sources {
291+
SourceStrategy::Enabled => script
292+
.metadata
293+
.tool
294+
.as_ref()
295+
.and_then(|tool| tool.uv.as_ref())
296+
.and_then(|uv| uv.top_level.index.as_deref())
297+
.unwrap_or(&empty),
298+
SourceStrategy::Disabled => &empty,
299+
};
300+
301+
// Collect any `tool.uv.sources` from the script.
302+
let empty = BTreeMap::default();
303+
let sources = match sources {
304+
SourceStrategy::Enabled => script
305+
.metadata
306+
.tool
307+
.as_ref()
308+
.and_then(|tool| tool.uv.as_ref())
309+
.and_then(|uv| uv.sources.as_ref())
310+
.unwrap_or(&empty),
311+
SourceStrategy::Disabled => &empty,
312+
};
313+
314+
Ok(requirements
315+
.into_iter()
316+
.flat_map(|requirement| {
317+
let requirement_name = requirement.name.clone();
318+
LoweredRequirement::from_non_workspace_requirement(
319+
requirement,
320+
script.path.parent().unwrap(),
321+
sources,
322+
indexes,
323+
locations,
324+
LowerBound::Allow,
325+
)
326+
.map(move |requirement| match requirement {
327+
Ok(requirement) => Ok(requirement.into_inner()),
328+
Err(err) => Err(uv_distribution::MetadataError::LoweringError(
329+
requirement_name.clone(),
330+
Box::new(err),
331+
)),
332+
})
333+
})
334+
.collect::<Result<_, _>>()?)
335+
}
226336
}
227337
}
228338
}

‎crates/uv/src/lib.rs

+16-2
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,12 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
183183
script: Some(script),
184184
..
185185
}) = &**command
186+
{
187+
Pep723Script::read(&script).await?.map(Pep723Item::Script)
188+
} else if let ProjectCommand::Lock(uv_cli::LockArgs {
189+
script: Some(script),
190+
..
191+
}) = &**command
186192
{
187193
Pep723Script::read(&script).await?.map(Pep723Item::Script)
188194
} else {
@@ -1483,14 +1489,22 @@ async fn run_project(
14831489
.combine(Refresh::from(args.settings.upgrade.clone())),
14841490
);
14851491

1486-
commands::lock(
1492+
// Unwrap the script.
1493+
let script = script.map(|script| match script {
1494+
Pep723Item::Script(script) => script,
1495+
Pep723Item::Stdin(_) => unreachable!("`uv lock` does not support stdin"),
1496+
Pep723Item::Remote(_) => unreachable!("`uv lock` does not support remote files"),
1497+
});
1498+
1499+
Box::pin(commands::lock(
14871500
project_dir,
14881501
args.locked,
14891502
args.frozen,
14901503
args.dry_run,
14911504
args.python,
14921505
args.install_mirrors,
14931506
args.settings,
1507+
script,
14941508
globals.python_preference,
14951509
globals.python_downloads,
14961510
globals.connectivity,
@@ -1501,7 +1515,7 @@ async fn run_project(
15011515
&cache,
15021516
printer,
15031517
globals.preview,
1504-
)
1518+
))
15051519
.await
15061520
}
15071521
ProjectCommand::Add(args) => {

0 commit comments

Comments
 (0)
Please sign in to comment.