mirror of https://github.com/astral-sh/uv
Add support for locking PEP 723 scripts (#10135)
## Summary You can now run `uv lock --script main.py` to lock a given script (though as of this PR, the script itself isn't used anywhere). Closes https://github.com/astral-sh/uv/issues/6318.
This commit is contained in:
parent
2f7f9ea571
commit
5d33b94c42
|
|
@ -3108,6 +3108,13 @@ pub struct LockArgs {
|
||||||
#[arg(long, conflicts_with = "check_exists", conflicts_with = "check")]
|
#[arg(long, conflicts_with = "check_exists", conflicts_with = "check")]
|
||||||
pub dry_run: bool,
|
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) to a `.lock` file adjacent to the script itself.
|
||||||
|
#[arg(long)]
|
||||||
|
pub script: Option<PathBuf>,
|
||||||
|
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub resolver: ResolverArgs,
|
pub resolver: ResolverArgs,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ use uv_resolver::{
|
||||||
FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, PythonRequirement, RequiresPython,
|
FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, PythonRequirement, RequiresPython,
|
||||||
ResolverEnvironment, ResolverManifest, SatisfiesResult, UniversalMarker,
|
ResolverEnvironment, ResolverManifest, SatisfiesResult, UniversalMarker,
|
||||||
};
|
};
|
||||||
|
use uv_scripts::{Pep723ItemRef, Pep723Script};
|
||||||
use uv_settings::PythonInstallMirrors;
|
use uv_settings::PythonInstallMirrors;
|
||||||
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
||||||
use uv_warnings::{warn_user, warn_user_once};
|
use uv_warnings::{warn_user, warn_user_once};
|
||||||
|
|
@ -39,7 +40,7 @@ use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceMember};
|
||||||
|
|
||||||
use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, SummaryResolveLogger};
|
use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, SummaryResolveLogger};
|
||||||
use crate::commands::project::lock_target::LockTarget;
|
use crate::commands::project::lock_target::LockTarget;
|
||||||
use crate::commands::project::{ProjectError, ProjectInterpreter};
|
use crate::commands::project::{ProjectError, ProjectInterpreter, ScriptInterpreter};
|
||||||
use crate::commands::reporters::ResolverReporter;
|
use crate::commands::reporters::ResolverReporter;
|
||||||
use crate::commands::{diagnostics, pip, ExitStatus};
|
use crate::commands::{diagnostics, pip, ExitStatus};
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
|
@ -80,6 +81,7 @@ pub(crate) async fn lock(
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
install_mirrors: PythonInstallMirrors,
|
install_mirrors: PythonInstallMirrors,
|
||||||
settings: ResolverSettings,
|
settings: ResolverSettings,
|
||||||
|
script: Option<Pep723Script>,
|
||||||
python_preference: PythonPreference,
|
python_preference: PythonPreference,
|
||||||
python_downloads: PythonDownloads,
|
python_downloads: PythonDownloads,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
|
|
@ -92,29 +94,52 @@ pub(crate) async fn lock(
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
) -> anyhow::Result<ExitStatus> {
|
) -> anyhow::Result<ExitStatus> {
|
||||||
// Find the project requirements.
|
// 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.
|
// Determine the lock mode.
|
||||||
let interpreter;
|
let interpreter;
|
||||||
let mode = if frozen {
|
let mode = if frozen {
|
||||||
LockMode::Frozen
|
LockMode::Frozen
|
||||||
} else {
|
} else {
|
||||||
interpreter = ProjectInterpreter::discover(
|
interpreter = match target {
|
||||||
&workspace,
|
LockTarget::Workspace(workspace) => ProjectInterpreter::discover(
|
||||||
project_dir,
|
workspace,
|
||||||
python.as_deref().map(PythonRequest::parse),
|
project_dir,
|
||||||
python_preference,
|
python.as_deref().map(PythonRequest::parse),
|
||||||
python_downloads,
|
python_preference,
|
||||||
connectivity,
|
python_downloads,
|
||||||
native_tls,
|
connectivity,
|
||||||
allow_insecure_host,
|
native_tls,
|
||||||
&install_mirrors,
|
allow_insecure_host,
|
||||||
no_config,
|
&install_mirrors,
|
||||||
cache,
|
no_config,
|
||||||
printer,
|
cache,
|
||||||
)
|
printer,
|
||||||
.await?
|
)
|
||||||
.into_interpreter();
|
.await?
|
||||||
|
.into_interpreter(),
|
||||||
|
LockTarget::Script(script) => ScriptInterpreter::discover(
|
||||||
|
Pep723ItemRef::Script(script),
|
||||||
|
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 {
|
if locked {
|
||||||
LockMode::Locked(&interpreter)
|
LockMode::Locked(&interpreter)
|
||||||
|
|
@ -131,7 +156,7 @@ pub(crate) async fn lock(
|
||||||
// Perform the lock operation.
|
// Perform the lock operation.
|
||||||
match do_safe_lock(
|
match do_safe_lock(
|
||||||
mode,
|
mode,
|
||||||
(&workspace).into(),
|
target,
|
||||||
settings.as_ref(),
|
settings.as_ref(),
|
||||||
LowerBound::Warn,
|
LowerBound::Warn,
|
||||||
&state,
|
&state,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use itertools::Either;
|
||||||
|
|
||||||
use uv_configuration::{LowerBound, SourceStrategy};
|
use uv_configuration::{LowerBound, SourceStrategy};
|
||||||
|
use uv_distribution::LoweredRequirement;
|
||||||
use uv_distribution_types::IndexLocations;
|
use uv_distribution_types::IndexLocations;
|
||||||
use uv_normalize::{GroupName, PackageName};
|
use uv_normalize::{GroupName, PackageName};
|
||||||
use uv_pep508::RequirementOrigin;
|
use uv_pep508::RequirementOrigin;
|
||||||
use uv_pypi_types::{Conflicts, Requirement, SupportedEnvironments, VerbatimParsedUrl};
|
use uv_pypi_types::{Conflicts, Requirement, SupportedEnvironments, VerbatimParsedUrl};
|
||||||
use uv_resolver::{Lock, LockVersion, RequiresPython, VERSION};
|
use uv_resolver::{Lock, LockVersion, RequiresPython, VERSION};
|
||||||
|
use uv_scripts::Pep723Script;
|
||||||
use uv_workspace::dependency_groups::DependencyGroupError;
|
use uv_workspace::dependency_groups::DependencyGroupError;
|
||||||
use uv_workspace::{Workspace, WorkspaceMember};
|
use uv_workspace::{Workspace, WorkspaceMember};
|
||||||
|
|
||||||
|
|
@ -16,6 +20,7 @@ use crate::commands::project::{find_requires_python, ProjectError};
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub(crate) enum LockTarget<'lock> {
|
pub(crate) enum LockTarget<'lock> {
|
||||||
Workspace(&'lock Workspace),
|
Workspace(&'lock Workspace),
|
||||||
|
Script(&'lock Pep723Script),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'lock> From<&'lock Workspace> for LockTarget<'lock> {
|
impl<'lock> From<&'lock Workspace> for LockTarget<'lock> {
|
||||||
|
|
@ -24,12 +29,19 @@ 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> {
|
impl<'lock> LockTarget<'lock> {
|
||||||
/// Return the set of requirements that are attached to the target directly, as opposed to being
|
/// Return the set of requirements that are attached to the target directly, as opposed to being
|
||||||
/// attached to any members within the target.
|
/// attached to any members within the target.
|
||||||
pub(crate) fn requirements(self) -> Vec<uv_pep508::Requirement<VerbatimParsedUrl>> {
|
pub(crate) fn requirements(self) -> Vec<uv_pep508::Requirement<VerbatimParsedUrl>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Workspace(workspace) => workspace.requirements(),
|
Self::Workspace(workspace) => workspace.requirements(),
|
||||||
|
Self::Script(script) => script.metadata.dependencies.clone().unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,6 +49,16 @@ impl<'lock> LockTarget<'lock> {
|
||||||
pub(crate) fn overrides(self) -> Vec<uv_pep508::Requirement<VerbatimParsedUrl>> {
|
pub(crate) fn overrides(self) -> Vec<uv_pep508::Requirement<VerbatimParsedUrl>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Workspace(workspace) => workspace.overrides(),
|
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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,6 +66,16 @@ impl<'lock> LockTarget<'lock> {
|
||||||
pub(crate) fn constraints(self) -> Vec<uv_pep508::Requirement<VerbatimParsedUrl>> {
|
pub(crate) fn constraints(self) -> Vec<uv_pep508::Requirement<VerbatimParsedUrl>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Workspace(workspace) => workspace.constraints(),
|
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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,20 +89,23 @@ impl<'lock> LockTarget<'lock> {
|
||||||
> {
|
> {
|
||||||
match self {
|
match self {
|
||||||
Self::Workspace(workspace) => workspace.dependency_groups(),
|
Self::Workspace(workspace) => workspace.dependency_groups(),
|
||||||
|
Self::Script(_) => Ok(BTreeMap::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the set of all members within the target.
|
/// Returns the set of all members within the target.
|
||||||
pub(crate) fn members_requirements(self) -> impl Iterator<Item = Requirement> + 'lock {
|
pub(crate) fn members_requirements(self) -> impl Iterator<Item = Requirement> + 'lock {
|
||||||
match self {
|
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 all dependency groups within the target.
|
/// Returns the set of all dependency groups within the target.
|
||||||
pub(crate) fn group_requirements(self) -> impl Iterator<Item = Requirement> + 'lock {
|
pub(crate) fn group_requirements(self) -> impl Iterator<Item = Requirement> + 'lock {
|
||||||
match self {
|
match self {
|
||||||
Self::Workspace(workspace) => workspace.group_requirements(),
|
Self::Workspace(workspace) => Either::Left(workspace.group_requirements()),
|
||||||
|
Self::Script(_) => Either::Right(std::iter::empty()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,6 +125,7 @@ impl<'lock> LockTarget<'lock> {
|
||||||
|
|
||||||
members
|
members
|
||||||
}
|
}
|
||||||
|
Self::Script(_) => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,6 +133,10 @@ impl<'lock> LockTarget<'lock> {
|
||||||
pub(crate) fn packages(self) -> &'lock BTreeMap<PackageName, WorkspaceMember> {
|
pub(crate) fn packages(self) -> &'lock BTreeMap<PackageName, WorkspaceMember> {
|
||||||
match self {
|
match self {
|
||||||
Self::Workspace(workspace) => workspace.packages(),
|
Self::Workspace(workspace) => workspace.packages(),
|
||||||
|
Self::Script(_) => {
|
||||||
|
static EMPTY: BTreeMap<PackageName, WorkspaceMember> = BTreeMap::new();
|
||||||
|
&EMPTY
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,6 +144,10 @@ impl<'lock> LockTarget<'lock> {
|
||||||
pub(crate) fn environments(self) -> Option<&'lock SupportedEnvironments> {
|
pub(crate) fn environments(self) -> Option<&'lock SupportedEnvironments> {
|
||||||
match self {
|
match self {
|
||||||
Self::Workspace(workspace) => workspace.environments(),
|
Self::Workspace(workspace) => workspace.environments(),
|
||||||
|
Self::Script(_) => {
|
||||||
|
// TODO(charlie): Add support for environments in scripts.
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,6 +155,7 @@ impl<'lock> LockTarget<'lock> {
|
||||||
pub(crate) fn conflicts(self) -> Conflicts {
|
pub(crate) fn conflicts(self) -> Conflicts {
|
||||||
match self {
|
match self {
|
||||||
Self::Workspace(workspace) => workspace.conflicts(),
|
Self::Workspace(workspace) => workspace.conflicts(),
|
||||||
|
Self::Script(_) => Conflicts::empty(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,6 +163,11 @@ impl<'lock> LockTarget<'lock> {
|
||||||
pub(crate) fn requires_python(self) -> Option<RequiresPython> {
|
pub(crate) fn requires_python(self) -> Option<RequiresPython> {
|
||||||
match self {
|
match self {
|
||||||
Self::Workspace(workspace) => find_requires_python(workspace),
|
Self::Workspace(workspace) => find_requires_python(workspace),
|
||||||
|
Self::Script(script) => script
|
||||||
|
.metadata
|
||||||
|
.requires_python
|
||||||
|
.as_ref()
|
||||||
|
.map(RequiresPython::from_specifiers),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,13 +175,24 @@ impl<'lock> LockTarget<'lock> {
|
||||||
pub(crate) fn install_path(self) -> &'lock Path {
|
pub(crate) fn install_path(self) -> &'lock Path {
|
||||||
match self {
|
match self {
|
||||||
Self::Workspace(workspace) => workspace.install_path(),
|
Self::Workspace(workspace) => workspace.install_path(),
|
||||||
|
Self::Script(script) => script.path.parent().unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the path to the lockfile.
|
/// Return the path to the lockfile.
|
||||||
pub(crate) fn lock_path(self) -> PathBuf {
|
pub(crate) fn lock_path(self) -> PathBuf {
|
||||||
match self {
|
match self {
|
||||||
|
// `uv.lock`
|
||||||
Self::Workspace(workspace) => workspace.install_path().join("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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,6 +284,55 @@ impl<'lock> LockTarget<'lock> {
|
||||||
.map(|requirement| requirement.with_origin(RequirementOrigin::Workspace))
|
.map(|requirement| requirement.with_origin(RequirementOrigin::Workspace))
|
||||||
.collect::<Vec<_>>())
|
.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<_, _>>()?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,12 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
||||||
script: Some(script),
|
script: Some(script),
|
||||||
..
|
..
|
||||||
}) = &**command
|
}) = &**command
|
||||||
|
{
|
||||||
|
Pep723Script::read(&script).await?.map(Pep723Item::Script)
|
||||||
|
} else if let ProjectCommand::Lock(uv_cli::LockArgs {
|
||||||
|
script: Some(script),
|
||||||
|
..
|
||||||
|
}) = &**command
|
||||||
{
|
{
|
||||||
Pep723Script::read(&script).await?.map(Pep723Item::Script)
|
Pep723Script::read(&script).await?.map(Pep723Item::Script)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1508,7 +1514,14 @@ async fn run_project(
|
||||||
.combine(Refresh::from(args.settings.upgrade.clone())),
|
.combine(Refresh::from(args.settings.upgrade.clone())),
|
||||||
);
|
);
|
||||||
|
|
||||||
commands::lock(
|
// Unwrap the script.
|
||||||
|
let script = script.map(|script| match script {
|
||||||
|
Pep723Item::Script(script) => script,
|
||||||
|
Pep723Item::Stdin(_) => unreachable!("`uv lock` does not support stdin"),
|
||||||
|
Pep723Item::Remote(_) => unreachable!("`uv lock` does not support remote files"),
|
||||||
|
});
|
||||||
|
|
||||||
|
Box::pin(commands::lock(
|
||||||
project_dir,
|
project_dir,
|
||||||
args.locked,
|
args.locked,
|
||||||
args.frozen,
|
args.frozen,
|
||||||
|
|
@ -1516,6 +1529,7 @@ async fn run_project(
|
||||||
args.python,
|
args.python,
|
||||||
args.install_mirrors,
|
args.install_mirrors,
|
||||||
args.settings,
|
args.settings,
|
||||||
|
script,
|
||||||
globals.python_preference,
|
globals.python_preference,
|
||||||
globals.python_downloads,
|
globals.python_downloads,
|
||||||
globals.connectivity,
|
globals.connectivity,
|
||||||
|
|
@ -1526,7 +1540,7 @@ async fn run_project(
|
||||||
&cache,
|
&cache,
|
||||||
printer,
|
printer,
|
||||||
globals.preview,
|
globals.preview,
|
||||||
)
|
))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
ProjectCommand::Add(args) => {
|
ProjectCommand::Add(args) => {
|
||||||
|
|
|
||||||
|
|
@ -1023,6 +1023,7 @@ pub(crate) struct LockSettings {
|
||||||
pub(crate) locked: bool,
|
pub(crate) locked: bool,
|
||||||
pub(crate) frozen: bool,
|
pub(crate) frozen: bool,
|
||||||
pub(crate) dry_run: bool,
|
pub(crate) dry_run: bool,
|
||||||
|
pub(crate) script: Option<PathBuf>,
|
||||||
pub(crate) python: Option<String>,
|
pub(crate) python: Option<String>,
|
||||||
pub(crate) install_mirrors: PythonInstallMirrors,
|
pub(crate) install_mirrors: PythonInstallMirrors,
|
||||||
pub(crate) refresh: Refresh,
|
pub(crate) refresh: Refresh,
|
||||||
|
|
@ -1037,6 +1038,7 @@ impl LockSettings {
|
||||||
check,
|
check,
|
||||||
check_exists,
|
check_exists,
|
||||||
dry_run,
|
dry_run,
|
||||||
|
script,
|
||||||
resolver,
|
resolver,
|
||||||
build,
|
build,
|
||||||
refresh,
|
refresh,
|
||||||
|
|
@ -1052,6 +1054,7 @@ impl LockSettings {
|
||||||
locked: check,
|
locked: check,
|
||||||
frozen: check_exists,
|
frozen: check_exists,
|
||||||
dry_run,
|
dry_run,
|
||||||
|
script,
|
||||||
python: python.and_then(Maybe::into_option),
|
python: python.and_then(Maybe::into_option),
|
||||||
refresh: Refresh::from(refresh),
|
refresh: Refresh::from(refresh),
|
||||||
settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem),
|
settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem),
|
||||||
|
|
|
||||||
|
|
@ -21360,6 +21360,254 @@ fn lock_missing_git_prefix() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lock_script() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let script = context.temp_dir.child("script.py");
|
||||||
|
script.write_str(indoc! { r#"
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.11"
|
||||||
|
# dependencies = [
|
||||||
|
# "anyio",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
|
||||||
|
import anyio
|
||||||
|
"#
|
||||||
|
})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.lock().arg("--script").arg("script.py"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let lock = context.read("script.py.lock");
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
lock, @r###"
|
||||||
|
version = 1
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
|
[options]
|
||||||
|
exclude-newer = "2024-03-25T00:00:00Z"
|
||||||
|
|
||||||
|
[manifest]
|
||||||
|
requirements = [{ name = "anyio" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "sniffio" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sniffio"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||||
|
]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-run with `--locked`.
|
||||||
|
uv_snapshot!(context.filters(), context.lock().arg("--script").arg("script.py").arg("--locked"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Modify the script metadata.
|
||||||
|
script.write_str(indoc! { r#"
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.11"
|
||||||
|
# dependencies = [
|
||||||
|
# "anyio",
|
||||||
|
# "iniconfig",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
|
||||||
|
import anyio
|
||||||
|
"#
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Re-run with `--locked`.
|
||||||
|
uv_snapshot!(context.filters(), context.lock().arg("--script").arg("script.py").arg("--locked"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 4 packages in [TIME]
|
||||||
|
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lock_script_path() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let script = context.temp_dir.child("script.py");
|
||||||
|
script.write_str(indoc! { r#"
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.11"
|
||||||
|
# dependencies = [
|
||||||
|
# "anyio",
|
||||||
|
# "child",
|
||||||
|
# ]
|
||||||
|
#
|
||||||
|
# [tool.uv.sources]
|
||||||
|
# child = { path = "child" }
|
||||||
|
# ///
|
||||||
|
|
||||||
|
import anyio
|
||||||
|
"#
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let child = context.temp_dir.child("child");
|
||||||
|
fs_err::create_dir_all(&child)?;
|
||||||
|
|
||||||
|
let pyproject_toml = child.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "child"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = ["iniconfig"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=42"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.lock().arg("--script").arg("script.py"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 5 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let lock = context.read("script.py.lock");
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
lock, @r###"
|
||||||
|
version = 1
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
|
[options]
|
||||||
|
exclude-newer = "2024-03-25T00:00:00Z"
|
||||||
|
|
||||||
|
[manifest]
|
||||||
|
requirements = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "child", directory = "child" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "sniffio" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "child"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { directory = "child" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [{ name = "iniconfig" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sniffio"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||||
|
]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-run with `--locked`.
|
||||||
|
uv_snapshot!(context.filters(), context.lock().arg("--script").arg("script.py").arg("--locked"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 5 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lock_pytorch_cpu() -> Result<()> {
|
fn lock_pytorch_cpu() -> Result<()> {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
|
|
|
||||||
|
|
@ -2135,6 +2135,10 @@ uv lock [OPTIONS]
|
||||||
|
|
||||||
<li><code>lowest-direct</code>: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies</li>
|
<li><code>lowest-direct</code>: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</dd><dt><code>--script</code> <i>script</i></dt><dd><p>Lock the specified Python script, rather than the current project.</p>
|
||||||
|
|
||||||
|
<p>If provided, uv will lock the script (based on its inline metadata table, in adherence with PEP 723) to a <code>.lock</code> file adjacent to the script itself.</p>
|
||||||
|
|
||||||
</dd><dt><code>--upgrade</code>, <code>-U</code></dt><dd><p>Allow package upgrades, ignoring pinned versions in any existing output file. Implies <code>--refresh</code></p>
|
</dd><dt><code>--upgrade</code>, <code>-U</code></dt><dd><p>Allow package upgrades, ignoring pinned versions in any existing output file. Implies <code>--refresh</code></p>
|
||||||
|
|
||||||
</dd><dt><code>--upgrade-package</code>, <code>-P</code> <i>upgrade-package</i></dt><dd><p>Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies <code>--refresh-package</code></p>
|
</dd><dt><code>--upgrade-package</code>, <code>-P</code> <i>upgrade-package</i></dt><dd><p>Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies <code>--refresh-package</code></p>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue