diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 1ec02fa391d5..8c0dc6f8cee9 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3651,6 +3651,14 @@ pub struct ExportArgs { #[command(flatten)] pub refresh: RefreshArgs, + /// Export the dependencies for the specified PEP 723 Python script, rather than the current + /// project. + /// + /// If provided, uv will resolve the dependencies based on its inline metadata table, in + /// adherence with PEP 723. + #[arg(long, conflicts_with_all = ["all_packages", "package", "no_emit_project", "no_emit_workspace"])] + pub script: Option, + /// The Python interpreter to use during resolution. /// /// A Python interpreter is required for building source distributions to diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 99378e28074c..950e7470af52 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -1003,8 +1003,8 @@ enum AddTarget { impl<'lock> From<&'lock AddTarget> for LockTarget<'lock> { fn from(value: &'lock AddTarget) -> Self { match value { - AddTarget::Script(script, _) => LockTarget::Script(script), - AddTarget::Project(project, _) => LockTarget::Workspace(project.workspace()), + AddTarget::Script(script, _) => Self::Script(script), + AddTarget::Project(project, _) => Self::Workspace(project.workspace()), } } } diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index eb1d0ebe3bb8..64b70d466915 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -16,19 +16,39 @@ use uv_dispatch::SharedState; use uv_normalize::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; use uv_resolver::RequirementsTxtExport; +use uv_scripts::{Pep723ItemRef, Pep723Script}; use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace}; use crate::commands::pip::loggers::DefaultResolveLogger; use crate::commands::project::install_target::InstallTarget; use crate::commands::project::lock::{do_safe_lock, LockMode}; +use crate::commands::project::lock_target::LockTarget; use crate::commands::project::{ default_dependency_groups, detect_conflicts, DependencyGroupsTarget, ProjectError, - ProjectInterpreter, + ProjectInterpreter, ScriptInterpreter, }; use crate::commands::{diagnostics, ExitStatus, OutputWriter}; use crate::printer::Printer; use crate::settings::ResolverSettings; +#[derive(Debug, Clone)] +enum ExportTarget { + /// A PEP 723 script, with inline metadata. + Script(Pep723Script), + + /// A project with a `pyproject.toml`. + Project(VirtualProject), +} + +impl<'lock> From<&'lock ExportTarget> for LockTarget<'lock> { + fn from(value: &'lock ExportTarget) -> Self { + match value { + ExportTarget::Script(script) => Self::Script(script), + ExportTarget::Project(project) => Self::Workspace(project.workspace()), + } + } +} + /// Export the project's `uv.lock` in an alternate format. #[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn export( @@ -46,6 +66,7 @@ pub(crate) async fn export( locked: bool, frozen: bool, include_header: bool, + script: Option, python: Option, install_mirrors: PythonInstallMirrors, settings: ResolverSettings, @@ -61,74 +82,108 @@ pub(crate) async fn export( printer: Printer, preview: PreviewMode, ) -> Result { - // Identify the project. - let project = if frozen { - VirtualProject::discover( - project_dir, - &DiscoveryOptions { - members: MemberDiscovery::None, - ..DiscoveryOptions::default() - }, - ) - .await? - } else if let Some(package) = package.as_ref() { - VirtualProject::Project( - Workspace::discover(project_dir, &DiscoveryOptions::default()) - .await? - .with_current_project(package.clone()) - .with_context(|| format!("Package `{package}` not found in workspace"))?, - ) + // Identify the target. + let target = if let Some(script) = script { + ExportTarget::Script(script) } else { - VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await? + let project = if frozen { + VirtualProject::discover( + project_dir, + &DiscoveryOptions { + members: MemberDiscovery::None, + ..DiscoveryOptions::default() + }, + ) + .await? + } else if let Some(package) = package.as_ref() { + VirtualProject::Project( + Workspace::discover(project_dir, &DiscoveryOptions::default()) + .await? + .with_current_project(package.clone()) + .with_context(|| format!("Package `{package}` not found in workspace"))?, + ) + } else { + VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await? + }; + ExportTarget::Project(project) }; // Validate that any referenced dependency groups are defined in the workspace. if !frozen { - let target = match &project { - VirtualProject::Project(project) => { + let target = match &target { + ExportTarget::Project(VirtualProject::Project(project)) => { if all_packages { DependencyGroupsTarget::Workspace(project.workspace()) } else { DependencyGroupsTarget::Project(project) } } - VirtualProject::NonProject(workspace) => DependencyGroupsTarget::Workspace(workspace), + ExportTarget::Project(VirtualProject::NonProject(workspace)) => { + DependencyGroupsTarget::Workspace(workspace) + } + ExportTarget::Script(_) => DependencyGroupsTarget::Script, }; target.validate(&dev)?; } // Determine the default groups to include. - let defaults = default_dependency_groups(project.pyproject_toml())?; + let defaults = match &target { + ExportTarget::Project(project) => default_dependency_groups(project.pyproject_toml())?, + ExportTarget::Script(_) => vec![], + }; let dev = dev.with_defaults(defaults); + // Find an interpreter for the project, unless `--frozen` is set. + let interpreter = if frozen { + None + } else { + Some(match &target { + ExportTarget::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(), + ExportTarget::Project(project) => 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(), + }) + }; + // Determine the lock mode. - let interpreter; let mode = if frozen { LockMode::Frozen + } else if locked { + LockMode::Locked(interpreter.as_ref().unwrap()) + } else if matches!(target, ExportTarget::Script(_)) + && !LockTarget::from(&target).lock_path().is_file() + { + // If we're locking a script, avoid creating a lockfile if it doesn't already exist. + LockMode::DryRun(interpreter.as_ref().unwrap()) } else { - // Find an interpreter for the project - 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(); - - if locked { - LockMode::Locked(&interpreter) - } else { - LockMode::Write(&interpreter) - } + LockMode::Write(interpreter.as_ref().unwrap()) }; // Initialize any shared state. @@ -137,7 +192,7 @@ pub(crate) async fn export( // Lock the project. let lock = match do_safe_lock( mode, - project.workspace().into(), + (&target).into(), settings.as_ref(), LowerBound::Warn, &state, @@ -165,8 +220,8 @@ pub(crate) async fn export( detect_conflicts(&lock, &extras, &dev)?; // Identify the installation target. - let target = match &project { - VirtualProject::Project(project) => { + let target = match &target { + ExportTarget::Project(VirtualProject::Project(project)) => { if all_packages { InstallTarget::Workspace { workspace: project.workspace(), @@ -187,7 +242,7 @@ pub(crate) async fn export( } } } - VirtualProject::NonProject(workspace) => { + ExportTarget::Project(VirtualProject::NonProject(workspace)) => { if all_packages { InstallTarget::NonProjectWorkspace { workspace, @@ -207,6 +262,10 @@ pub(crate) async fn export( } } } + ExportTarget::Script(script) => InstallTarget::Script { + script, + lock: &lock, + }, }; // Write the resolved dependencies to the output channel. diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index e51c2afb4b5b..69d4e6f414a3 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -197,6 +197,12 @@ async fn run(mut cli: Cli) -> Result { script: Some(script), .. }) = &**command + { + Pep723Script::read(&script).await?.map(Pep723Item::Script) + } else if let ProjectCommand::Export(uv_cli::ExportArgs { + script: Some(script), + .. + }) = &**command { Pep723Script::read(&script).await?.map(Pep723Item::Script) } else { @@ -1704,6 +1710,13 @@ async fn run_project( // Initialize the cache. let cache = cache.init()?; + // Unwrap the script. + let script = script.map(|script| match script { + Pep723Item::Script(script) => script, + Pep723Item::Stdin(_) => unreachable!("`uv export` does not support stdin"), + Pep723Item::Remote(_) => unreachable!("`uv export` does not support remote files"), + }); + commands::export( project_dir, args.format, @@ -1719,6 +1732,7 @@ async fn run_project( args.locked, args.frozen, args.include_header, + script, args.python, args.install_mirrors, args.settings, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 8d6da1557432..1e282657dd31 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1369,6 +1369,7 @@ pub(crate) struct ExportSettings { pub(crate) locked: bool, pub(crate) frozen: bool, pub(crate) include_header: bool, + pub(crate) script: Option, pub(crate) python: Option, pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) refresh: Refresh, @@ -1409,6 +1410,7 @@ impl ExportSettings { resolver, build, refresh, + script, python, } = args; let install_mirrors = filesystem @@ -1440,6 +1442,7 @@ impl ExportSettings { locked, frozen, include_header: flag(header, no_header).unwrap_or(true), + 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/export.rs b/crates/uv/tests/it/export.rs index 19e7a388d937..aedf471d36f7 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -4,6 +4,8 @@ use crate::common::{apply_filters, uv_snapshot, TestContext}; use anyhow::{Ok, Result}; use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; +use indoc::indoc; +use insta::assert_snapshot; use std::process::Stdio; #[test] @@ -2054,6 +2056,263 @@ fn export_group() -> Result<()> { Ok(()) } +#[test] +fn 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==2.0.0 ; sys_platform == 'win32'", + # "anyio==3.0.0 ; sys_platform == 'linux'" + # ] + # /// + "#})?; + + uv_snapshot!(context.filters(), context.export().arg("--script").arg(script.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --script [TEMP_DIR]/script.py + anyio==2.0.0 ; sys_platform == 'win32' \ + --hash=sha256:0b8375c8fc665236cb4d143ea13e849eb9e074d727b1b5c27d88aba44ca8c547 \ + --hash=sha256:ceca4669ffa3f02bf20ef3d6c2a0c323b16cdc71d1ce0b0bc03c6f1f36054826 + anyio==3.0.0 ; sys_platform == 'linux' \ + --hash=sha256:b553598332c050af19f7d41f73a7790142f5bc3d5eb8bd82f5e515ec22019bd9 \ + --hash=sha256:e71c3d9d72291d12056c0265d07c6bbedf92332f78573e278aeb116f24f30395 + idna==3.6 ; sys_platform == 'linux' or sys_platform == 'win32' \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + sniffio==1.3.1 ; sys_platform == 'linux' or sys_platform == 'win32' \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + // If the lockfile didn't exist already, it shouldn't be persisted to disk. + assert!(!context.temp_dir.child("uv.lock").exists()); + + // Explicitly lock the script. + uv_snapshot!(context.filters(), context.lock().arg("--script").arg(script.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 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" + resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform == 'linux'", + "sys_platform != 'linux' and sys_platform != 'win32'", + ] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + requirements = [ + { name = "anyio", marker = "sys_platform == 'linux'", specifier = "==3.0.0" }, + { name = "anyio", marker = "sys_platform == 'win32'", specifier = "==2.0.0" }, + ] + + [[package]] + name = "anyio" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + resolution-markers = [ + "sys_platform == 'win32'", + ] + dependencies = [ + { name = "idna", marker = "sys_platform == 'win32'" }, + { name = "sniffio", marker = "sys_platform == 'win32'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/fe/dc/daeadb9b34093d3968afcc93946ee567cd6d2b402a96c608cb160f74d737/anyio-2.0.0.tar.gz", hash = "sha256:ceca4669ffa3f02bf20ef3d6c2a0c323b16cdc71d1ce0b0bc03c6f1f36054826", size = 91291 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/19/10fe682e962efd1610aa41376399fc3f3e002425449b02d0fb04749bb712/anyio-2.0.0-py3-none-any.whl", hash = "sha256:0b8375c8fc665236cb4d143ea13e849eb9e074d727b1b5c27d88aba44ca8c547", size = 62675 }, + ] + + [[package]] + name = "anyio" + version = "3.0.0" + source = { registry = "https://pypi.org/simple" } + resolution-markers = [ + "sys_platform == 'linux'", + ] + dependencies = [ + { name = "idna", marker = "sys_platform == 'linux'" }, + { name = "sniffio", marker = "sys_platform == 'linux'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/99/0d/65165f99e5f4f3b4c43a5ed9db0fb7aa655f5a58f290727a30528a87eb45/anyio-3.0.0.tar.gz", hash = "sha256:b553598332c050af19f7d41f73a7790142f5bc3d5eb8bd82f5e515ec22019bd9", size = 116952 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/49/ebee263b69fe243bd1fd0a88bc6bb0f7732bf1794ba3273cb446351f9482/anyio-3.0.0-py3-none-any.whl", hash = "sha256:e71c3d9d72291d12056c0265d07c6bbedf92332f78573e278aeb116f24f30395", size = 72182 }, + ] + + [[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 }, + ] + "### + ); + }); + + // Update the dependencies. + script.write_str(indoc! {r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "anyio==2.0.0 ; sys_platform == 'win32'", + # "anyio==3.0.0 ; sys_platform == 'linux'", + # "iniconfig", + # ] + # /// + "#})?; + + // `uv tree` should update the lockfile. + uv_snapshot!(context.filters(), context.export().arg("--script").arg(script.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --script [TEMP_DIR]/script.py + anyio==2.0.0 ; sys_platform == 'win32' \ + --hash=sha256:0b8375c8fc665236cb4d143ea13e849eb9e074d727b1b5c27d88aba44ca8c547 \ + --hash=sha256:ceca4669ffa3f02bf20ef3d6c2a0c323b16cdc71d1ce0b0bc03c6f1f36054826 + anyio==3.0.0 ; sys_platform == 'linux' \ + --hash=sha256:b553598332c050af19f7d41f73a7790142f5bc3d5eb8bd82f5e515ec22019bd9 \ + --hash=sha256:e71c3d9d72291d12056c0265d07c6bbedf92332f78573e278aeb116f24f30395 + idna==3.6 ; sys_platform == 'linux' or sys_platform == 'win32' \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + sniffio==1.3.1 ; sys_platform == 'linux' or sys_platform == 'win32' \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + + ----- 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" + resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform == 'linux'", + "sys_platform != 'linux' and sys_platform != 'win32'", + ] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + requirements = [ + { name = "anyio", marker = "sys_platform == 'linux'", specifier = "==3.0.0" }, + { name = "anyio", marker = "sys_platform == 'win32'", specifier = "==2.0.0" }, + { name = "iniconfig" }, + ] + + [[package]] + name = "anyio" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + resolution-markers = [ + "sys_platform == 'win32'", + ] + dependencies = [ + { name = "idna", marker = "sys_platform == 'win32'" }, + { name = "sniffio", marker = "sys_platform == 'win32'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/fe/dc/daeadb9b34093d3968afcc93946ee567cd6d2b402a96c608cb160f74d737/anyio-2.0.0.tar.gz", hash = "sha256:ceca4669ffa3f02bf20ef3d6c2a0c323b16cdc71d1ce0b0bc03c6f1f36054826", size = 91291 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/19/10fe682e962efd1610aa41376399fc3f3e002425449b02d0fb04749bb712/anyio-2.0.0-py3-none-any.whl", hash = "sha256:0b8375c8fc665236cb4d143ea13e849eb9e074d727b1b5c27d88aba44ca8c547", size = 62675 }, + ] + + [[package]] + name = "anyio" + version = "3.0.0" + source = { registry = "https://pypi.org/simple" } + resolution-markers = [ + "sys_platform == 'linux'", + ] + dependencies = [ + { name = "idna", marker = "sys_platform == 'linux'" }, + { name = "sniffio", marker = "sys_platform == 'linux'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/99/0d/65165f99e5f4f3b4c43a5ed9db0fb7aa655f5a58f290727a30528a87eb45/anyio-3.0.0.tar.gz", hash = "sha256:b553598332c050af19f7d41f73a7790142f5bc3d5eb8bd82f5e515ec22019bd9", size = 116952 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/49/ebee263b69fe243bd1fd0a88bc6bb0f7732bf1794ba3273cb446351f9482/anyio-3.0.0-py3-none-any.whl", hash = "sha256:e71c3d9d72291d12056c0265d07c6bbedf92332f78573e278aeb116f24f30395", size = 72182 }, + ] + + [[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 }, + ] + "### + ); + }); + + Ok(()) +} + #[test] fn conflicts() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index e8efbbea0cbf..ed6db02acf7b 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -2532,6 +2532,10 @@ uv export [OPTIONS]
  • lowest-direct: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies
  • +
    --script script

    Export the dependencies for the specified PEP 723 Python script, rather than the current project.

    + +

    If provided, uv will resolve the dependencies 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