Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6853,11 +6853,26 @@ pub enum WorkspaceCommand {
/// Display package metadata.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(My own doc here is bad, I should fix that)

#[command(hide = true)]
Metadata(MetadataArgs),
/// Display the path of a workspace member.
///
/// By default, the path to the workspace root directory is displayed.
/// The `--package` option can be used to display the path to a workspace member instead.
///
/// If used outside of a workspace, i.e., if a `pyproject.toml` cannot be found, uv will exit with an error.
#[command(hide = true)]
Dir(WorkspaceDirArgs),
}

#[derive(Args, Debug)]
pub struct MetadataArgs;

#[derive(Args, Debug)]
pub struct WorkspaceDirArgs {
/// Display the path to a specific package in the workspace.
#[arg(long)]
pub package: Option<PackageName>,
}

/// See [PEP 517](https://peps.python.org/pep-0517/) and
/// [PEP 660](https://peps.python.org/pep-0660/) for specifications of the parameters.
#[derive(Subcommand)]
Expand Down
3 changes: 3 additions & 0 deletions crates/uv-preview/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ bitflags::bitflags! {
const CACHE_SIZE = 1 << 11;
const INIT_PROJECT_FLAG = 1 << 12;
const WORKSPACE_METADATA = 1 << 13;
const WORKSPACE_DIR = 1 << 14;
}
}

Expand All @@ -46,6 +47,7 @@ impl PreviewFeatures {
Self::CACHE_SIZE => "cache-size",
Self::INIT_PROJECT_FLAG => "init-project-flag",
Self::WORKSPACE_METADATA => "workspace-metadata",
Self::WORKSPACE_DIR => "workspace-dir",
_ => panic!("`flag_as_str` can only be used for exactly one feature flag"),
}
}
Expand Down Expand Up @@ -97,6 +99,7 @@ impl FromStr for PreviewFeatures {
"cache-size" => Self::CACHE_SIZE,
"init-project-flag" => Self::INIT_PROJECT_FLAG,
"workspace-metadata" => Self::WORKSPACE_METADATA,
"workspace-dir" => Self::WORKSPACE_DIR,
_ => {
warn_user_once!("Unknown preview feature: `{part}`");
continue;
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ use uv_normalize::PackageName;
use uv_python::PythonEnvironment;
use uv_scripts::Pep723Script;
pub(crate) use venv::venv;
pub(crate) use workspace::dir::dir;
pub(crate) use workspace::metadata::metadata;

use crate::printer::Printer;
Expand Down
46 changes: 46 additions & 0 deletions crates/uv/src/commands/workspace/dir.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use anstream::println;
use std::path::Path;

use anyhow::{Result, bail};

use owo_colors::OwoColorize;
use uv_fs::Simplified;
use uv_normalize::PackageName;
use uv_preview::{Preview, PreviewFeatures};
use uv_warnings::warn_user;
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache};

use crate::commands::ExitStatus;

/// Print the path to the workspace dir
pub(crate) async fn dir(
package_name: Option<PackageName>,
project_dir: &Path,
preview: Preview,
) -> Result<ExitStatus> {
if preview.is_enabled(PreviewFeatures::WORKSPACE_DIR) {
warn_user!(
"The `uv workspace dir` command is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeatures::WORKSPACE_DIR
);
}

let workspace_cache = WorkspaceCache::default();
let workspace =
Workspace::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache).await?;

let dir: &Path = match package_name {
None => workspace.install_path().as_path(),
Some(package) => {
if let Some(p) = workspace.packages().get(&package) {
p.root().as_path()
} else {
bail!("Package `{package}` not found in workspace.")
}
}
};
Comment on lines +34 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code compiles without the type cast:

Suggested change
let dir: &Path = match package_name {
None => workspace.install_path().as_path(),
Some(package) => {
if let Some(p) = workspace.packages().get(&package) {
p.root().as_path()
} else {
bail!("Package `{package}` not found in workspace.")
}
}
};
let dir = match package_name {
None => workspace.install_path(),
Some(package) => {
if let Some(p) = workspace.packages().get(&package) {
p.root()
} else {
bail!("Package `{package}` not found in workspace.")
}
}
};


println!("{}", dir.simplified_display().cyan());

Ok(ExitStatus::Success)
}
1 change: 1 addition & 0 deletions crates/uv/src/commands/workspace/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub(crate) mod dir;
pub(crate) mod metadata;
3 changes: 3 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1738,6 +1738,9 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
WorkspaceCommand::Metadata(_args) => {
commands::metadata(&project_dir, globals.preview, printer).await
}
WorkspaceCommand::Dir(args) => {
commands::dir(args.package, &project_dir, globals.preview).await
}
},
Commands::BuildBackend { command } => spawn_blocking(move || match command {
BuildBackendCommand::BuildSdist { sdist_directory } => {
Expand Down
8 changes: 8 additions & 0 deletions crates/uv/tests/it/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,14 @@ impl TestContext {
command
}

/// Create a `uv workspace dir` command with options shared across scenarios.
pub fn workspace_dir(&self) -> Command {
let mut command = Self::new_command();
command.arg("workspace").arg("dir");
self.add_shared_options(&mut command, false);
command
}

/// Create a `uv export` command with options shared across scenarios.
pub fn export(&self) -> Command {
let mut command = Self::new_command();
Expand Down
1 change: 1 addition & 0 deletions crates/uv/tests/it/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,5 @@ mod workflow;

mod extract;
mod workspace;
mod workspace_dir;
mod workspace_metadata;
4 changes: 2 additions & 2 deletions crates/uv/tests/it/show_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7831,7 +7831,7 @@ fn preview_features() {
show_settings: true,
preview: Preview {
flags: PreviewFeatures(
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA,
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR,
),
},
python_preference: Managed,
Expand Down Expand Up @@ -8059,7 +8059,7 @@ fn preview_features() {
show_settings: true,
preview: Preview {
flags: PreviewFeatures(
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA,
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR,
),
},
python_preference: Managed,
Expand Down
123 changes: 123 additions & 0 deletions crates/uv/tests/it/workspace_dir.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use std::env;

use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use assert_fs::fixture::PathChild;

use crate::common::{TestContext, copy_dir_ignore, uv_snapshot};

/// Test basic output for a simple workspace with one member.
#[test]
fn workspace_dir_simple() {
let context = TestContext::new("3.12");

// Initialize a workspace with one member
context.init().arg("foo").assert().success();

let workspace = context.temp_dir.child("foo");

uv_snapshot!(context.filters(), context.workspace_dir().current_dir(&workspace), @r###"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/foo
----- stderr -----
"###
);
}

// Workspace dir output when run with `--package`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
// Workspace dir output when run with `--package`
/// Workspace dir output when run with `--package`.

#[test]
fn workspace_dir_specific_package() {
let context = TestContext::new("3.12");
context.init().arg("foo").assert().success();
context.init().arg("foo/bar").assert().success();
let workspace = context.temp_dir.child("foo");

// root workspace
uv_snapshot!(context.filters(), context.workspace_dir().current_dir(&workspace), @r###"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/foo
----- stderr -----
"###
);

// with --package bar
uv_snapshot!(context.filters(), context.workspace_dir().arg("--package").arg("bar").current_dir(&workspace), @r###"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/foo/bar
----- stderr -----
"###
);
}

/// Test output when run from a workspace member directory.
#[test]
fn workspace_metadata_from_member() -> Result<()> {
let context = TestContext::new("3.12");
let workspace = context.temp_dir.child("workspace");

let albatross_workspace = context
.workspace_root
.join("scripts/workspaces/albatross-root-workspace");

copy_dir_ignore(albatross_workspace, &workspace)?;

let member_dir = workspace.join("packages").join("bird-feeder");

uv_snapshot!(context.filters(), context.workspace_dir().current_dir(&member_dir), @r###"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/workspace
----- stderr -----
"###
);

Ok(())
}

/// Test workspace dir error output for a non-existent package.
#[test]
fn workspace_dir_package_doesnt_exist() {
let context = TestContext::new("3.12");

// Initialize a workspace with one member
context.init().arg("foo").assert().success();

let workspace = context.temp_dir.child("foo");

uv_snapshot!(context.filters(), context.workspace_dir().arg("--package").arg("bar").current_dir(&workspace), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Package `bar` not found in workspace.
"###
);
}

/// Test workspace dir error output when not in a project.
#[test]
fn workspace_metadata_no_project() {
let context = TestContext::new("3.12");

uv_snapshot!(context.filters(), context.workspace_dir(), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No `pyproject.toml` found in current directory or any parent directory
"###
);
}
1 change: 1 addition & 0 deletions docs/concepts/preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ The following preview features are available:
- `native-auth`: Enables storage of credentials in a
[system-native location](../concepts/authentication/http.md#the-uv-credentials-store).
- `workspace-metadata`: Allows using `uv workspace metadata`.
- `workspace-dir`: Allows using `uv workspace dir`.

## Disabling preview features

Expand Down
Loading