diff --git a/crates/uv/src/commands/project/version.rs b/crates/uv/src/commands/project/version.rs index efba226b9d948..c4b32485d3605 100644 --- a/crates/uv/src/commands/project/version.rs +++ b/crates/uv/src/commands/project/version.rs @@ -21,7 +21,7 @@ use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; use uv_settings::PythonInstallMirrors; use uv_workspace::pyproject_mut::Error; use uv_workspace::{ - DiscoveryOptions, WorkspaceCache, + DiscoveryOptions, WorkspaceCache, WorkspaceError, pyproject_mut::{DependencyTarget, PyProjectTomlMut}, }; use uv_workspace::{VirtualProject, Workspace}; @@ -59,6 +59,7 @@ pub(crate) async fn project_version( output_format: VersionFormat, project_dir: &Path, package: Option, + explicit_project: bool, dry_run: bool, locked: bool, frozen: bool, @@ -78,7 +79,7 @@ pub(crate) async fn project_version( preview: PreviewMode, ) -> Result { // Read the metadata - let project = find_target(project_dir, package.as_ref()).await?; + let project = find_target(project_dir, package.as_ref(), explicit_project).await?; let pyproject_path = project.root().join("pyproject.toml"); let Some(name) = project.project_name().cloned() else { @@ -325,10 +326,30 @@ pub(crate) async fn project_version( Ok(status) } +/// Add hint to use `uv self version` when workspace discovery fails due to missing pyproject.toml +/// and --project was not explicitly passed +fn hint_uv_self_version(err: WorkspaceError, explicit_project: bool) -> anyhow::Error { + if matches!(err, WorkspaceError::MissingPyprojectToml) && !explicit_project { + anyhow!( + "{}\n\n{}{} If you meant to view uv's version, use `{}` instead", + err, + "hint".bold().cyan(), + ":".bold(), + "uv self version".green() + ) + } else { + err.into() + } +} + /// Find the pyproject.toml we're modifying /// /// Note that `uv version` never needs to support PEP 723 scripts, as those are unversioned. -async fn find_target(project_dir: &Path, package: Option<&PackageName>) -> Result { +async fn find_target( + project_dir: &Path, + package: Option<&PackageName>, + explicit_project: bool, +) -> Result { // Find the project in the workspace. // No workspace caching since `uv version` changes the workspace definition. let project = if let Some(package) = package { @@ -338,7 +359,8 @@ async fn find_target(project_dir: &Path, package: Option<&PackageName>) -> Resul &DiscoveryOptions::default(), &WorkspaceCache::default(), ) - .await? + .await + .map_err(|err| hint_uv_self_version(err, explicit_project))? .with_current_project(package.clone()) .with_context(|| format!("Package `{package}` not found in workspace"))?, ) @@ -348,7 +370,8 @@ async fn find_target(project_dir: &Path, package: Option<&PackageName>) -> Resul &DiscoveryOptions::default(), &WorkspaceCache::default(), ) - .await? + .await + .map_err(|err| hint_uv_self_version(err, explicit_project))? }; Ok(project) } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 0f6c9465f95b2..4446572949abe 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1073,6 +1073,7 @@ async fn run(mut cli: Cli) -> Result { script, globals, cli.top_level.no_config, + cli.top_level.global_args.project.is_some(), filesystem, cache, printer, @@ -1674,6 +1675,7 @@ async fn run_project( globals: GlobalSettings, // TODO(zanieb): Determine a better story for passing `no_config` in here no_config: bool, + explicit_project: bool, filesystem: Option, cache: Cache, printer: Printer, @@ -2065,6 +2067,7 @@ async fn run_project( args.output_format, project_dir, args.package, + explicit_project, args.dry_run, args.locked, args.frozen, diff --git a/crates/uv/tests/it/version.rs b/crates/uv/tests/it/version.rs index 78dd6425280a2..0ea432c1cda2d 100644 --- a/crates/uv/tests/it/version.rs +++ b/crates/uv/tests/it/version.rs @@ -1687,20 +1687,20 @@ fn version_get_fallback_missing_strict() -> Result<()> { Ok(()) } -// Should error if this pyproject.toml is missing -// and --preview was passed explicitly. +/// Should error with hint if pyproject.toml is missing in normal mode #[test] -fn version_get_fallback_missing_strict_preview() -> Result<()> { +fn version_get_missing_with_hint() -> Result<()> { let context = TestContext::new("3.12"); - uv_snapshot!(context.filters(), context.version() - .arg("--preview"), @r" + uv_snapshot!(context.filters(), context.version(), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- error: No `pyproject.toml` found in current directory or any parent directory + + hint: If you meant to view uv's version, use `uv self version` instead "); Ok(())