diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index cc2bd21e5d2e..18c5bbc7a215 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -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, + #[command(flatten)] pub resolver: ResolverArgs, diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index 8b46007e0be8..923f101008dc 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -1,8 +1,8 @@ use std::collections::BTreeMap; -use std::io; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::LazyLock; +use std::{io, path}; use memchr::memmem::Finder; use serde::Deserialize; @@ -95,7 +95,7 @@ impl Pep723Script { let metadata = Pep723Metadata::from_str(&metadata)?; Ok(Some(Self { - path: file.as_ref().to_path_buf(), + path: path::absolute(file.as_ref())?, metadata, prelude, postlude, @@ -124,7 +124,7 @@ impl Pep723Script { let (shebang, postlude) = extract_shebang(&contents)?; Ok(Self { - path: file.as_ref().to_path_buf(), + path: path::absolute(file.as_ref())?, prelude: if shebang.is_empty() { String::new() } else { diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index b77ab955f0f9..430a293c6a4f 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -31,6 +31,7 @@ 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}; @@ -38,7 +39,7 @@ 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}; +use crate::commands::project::{ProjectError, ProjectInterpreter, ScriptInterpreter}; use crate::commands::reporters::ResolverReporter; use crate::commands::{diagnostics, pip, ExitStatus}; use crate::printer::Printer; @@ -79,6 +80,7 @@ pub(crate) async fn lock( python: Option, install_mirrors: PythonInstallMirrors, settings: ResolverSettings, + script: Option, python_preference: PythonPreference, python_downloads: PythonDownloads, connectivity: Connectivity, @@ -91,29 +93,52 @@ pub(crate) async fn lock( preview: PreviewMode, ) -> anyhow::Result { // 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) @@ -130,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, diff --git a/crates/uv/src/commands/project/lock_target.rs b/crates/uv/src/commands/project/lock_target.rs index 6b1dee8271e8..2d2cc0197adb 100644 --- a/crates/uv/src/commands/project/lock_target.rs +++ b/crates/uv/src/commands/project/lock_target.rs @@ -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::{GroupName, 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}; @@ -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> { @@ -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> { /// Return the set of requirements that are attached to the target directly, as opposed to being /// attached to any members within the target. pub(crate) fn requirements(self) -> Vec> { match self { 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> { 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(), } } @@ -44,6 +66,16 @@ impl<'lock> LockTarget<'lock> { pub(crate) fn constraints(self) -> Vec> { 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(), } } @@ -57,20 +89,23 @@ impl<'lock> LockTarget<'lock> { > { match self { Self::Workspace(workspace) => workspace.dependency_groups(), + Self::Script(_) => Ok(BTreeMap::new()), } } /// Returns the set of all members within the target. pub(crate) fn members_requirements(self) -> impl Iterator + '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 all dependency groups within the target. pub(crate) fn group_requirements(self) -> impl Iterator + 'lock { 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 } + Self::Script(_) => Vec::new(), } } @@ -97,6 +133,10 @@ impl<'lock> LockTarget<'lock> { pub(crate) fn packages(self) -> &'lock BTreeMap { match self { Self::Workspace(workspace) => workspace.packages(), + Self::Script(_) => { + static EMPTY: BTreeMap = BTreeMap::new(); + &EMPTY + } } } @@ -104,6 +144,10 @@ impl<'lock> LockTarget<'lock> { 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 + } } } @@ -111,6 +155,7 @@ impl<'lock> LockTarget<'lock> { pub(crate) fn conflicts(self) -> Conflicts { match self { Self::Workspace(workspace) => workspace.conflicts(), + Self::Script(_) => Conflicts::empty(), } } @@ -118,6 +163,11 @@ impl<'lock> LockTarget<'lock> { pub(crate) fn requires_python(self) -> Option { match self { 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 { match self { Self::Workspace(workspace) => workspace.install_path(), + Self::Script(script) => script.path.parent().unwrap(), } } /// Return the path to the lockfile. pub(crate) 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) + } } } @@ -223,6 +284,55 @@ impl<'lock> LockTarget<'lock> { .map(|requirement| requirement.with_origin(RequirementOrigin::Workspace)) .collect::>()) } + 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::>()?) + } } } } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index f34ef7a03a15..4092cd603c93 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -183,6 +183,12 @@ async fn run(mut cli: Cli) -> Result { script: Some(script), .. }) = &**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) } else { @@ -1483,7 +1489,14 @@ async fn run_project( .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, args.locked, args.frozen, @@ -1491,6 +1504,7 @@ async fn run_project( args.python, args.install_mirrors, args.settings, + script, globals.python_preference, globals.python_downloads, globals.connectivity, @@ -1501,7 +1515,7 @@ async fn run_project( &cache, printer, globals.preview, - ) + )) .await } ProjectCommand::Add(args) => { diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 80ac58595436..4ecc4d3ec2c3 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1008,6 +1008,7 @@ pub(crate) struct LockSettings { pub(crate) locked: bool, pub(crate) frozen: bool, pub(crate) dry_run: bool, + pub(crate) script: Option, pub(crate) python: Option, pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) refresh: Refresh, @@ -1022,6 +1023,7 @@ impl LockSettings { check, check_exists, dry_run, + script, resolver, build, refresh, @@ -1037,6 +1039,7 @@ impl LockSettings { locked: check, frozen: check_exists, dry_run, + script, python: python.and_then(Maybe::into_option), refresh: Refresh::from(refresh), settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem), diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 5825c090a482..ea7168f557e5 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -21359,6 +21359,254 @@ fn lock_missing_git_prefix() -> Result<()> { 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] fn lock_pytorch_cpu() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index cf8110eec7f8..5090d483a9f4 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -2123,6 +2123,10 @@ uv lock [OPTIONS]
  • lowest-direct: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies
  • +
    --script script

    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.

    +
    --upgrade, -U

    Allow package upgrades, ignoring pinned versions in any existing output file. Implies --refresh

    --upgrade-package, -P upgrade-package

    Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies --refresh-package