From 177bd99692451346cb41772ea7dd658b0bc3e86a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 25 Dec 2024 14:13:07 -0500 Subject: [PATCH] Add export support for PEP 723 scripts --- crates/uv-cli/src/lib.rs | 8 ++ crates/uv/src/commands/project/add.rs | 4 +- crates/uv/src/commands/project/export.rs | 141 +++++++++++++++-------- crates/uv/src/lib.rs | 18 ++- crates/uv/src/settings.rs | 3 + docs/reference/cli.md | 8 ++ 6 files changed, 133 insertions(+), 49 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 51eeb04cbbde..f7c92cab3f2a 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3626,6 +3626,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 02cb482ddddd..21c2868553cc 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -998,8 +998,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..7a69beffa6ce 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::{Pep723Item, 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,44 +82,55 @@ 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); // Determine the lock mode. @@ -106,24 +138,39 @@ pub(crate) async fn export( let mode = if frozen { LockMode::Frozen } 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(); - + interpreter = match &target { + ExportTarget::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(), + 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(), + }; if locked { LockMode::Locked(&interpreter) } else { @@ -137,7 +184,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 +212,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 +234,7 @@ pub(crate) async fn export( } } } - VirtualProject::NonProject(workspace) => { + ExportTarget::Project(VirtualProject::NonProject(workspace)) => { if all_packages { InstallTarget::NonProjectWorkspace { workspace, @@ -207,6 +254,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 407d91de05bf..a464ae68f372 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -195,6 +195,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 { @@ -1679,7 +1685,14 @@ async fn run_project( // Initialize the cache. let cache = cache.init()?; - commands::export( + // 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"), + }); + + Box::pin(commands::export( project_dir, args.format, args.all_packages, @@ -1694,6 +1707,7 @@ async fn run_project( args.locked, args.frozen, args.include_header, + script, args.python, args.install_mirrors, args.settings, @@ -1708,7 +1722,7 @@ async fn run_project( &cache, printer, globals.preview, - ) + )) .await } } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 8f20f2bc8218..a8586f7e81af 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1349,6 +1349,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, @@ -1389,6 +1390,7 @@ impl ExportSettings { resolver, build, refresh, + script, python, } = args; let install_mirrors = filesystem @@ -1420,6 +1422,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/docs/reference/cli.md b/docs/reference/cli.md index 5090d483a9f4..cf9111bbf577 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -2519,6 +2519,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

    @@ -2941,6 +2945,10 @@ uv tree [OPTIONS]
  • lowest-direct: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies
  • +
    --script script

    Show the dependency tree 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.

    +
    --universal

    Show a platform-independent dependency tree.

    Shows resolved package versions for all Python versions and platforms, rather than filtering to those that are relevant for the current environment.