mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-19 11:35:36 +00:00
workspace dir command (#16678)
Addresses https://github.com/astral-sh/uv/issues/13636 Prints the path to the workspace root by default, and any of the child packages if requested. I looped it into the same preview flag as `workspace metadata`, given how closely related they are. ## Summary ``` ─> uv workspace dir /Users/mikayla/code/uv/dev-envs ─> uv workspace dir --package foo-proj /Users/mikayla/code/uv/dev-envs/foo-proj ─> uv workspace dir --package bar-proj error: Package `bar-proj` not found in workspace. ``` ## Test Plan Unit tests added. --------- Signed-off-by: Mikayla Thompson <mrt@mikayla.codes> Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
parent
92c2bfcca0
commit
b81060674e
11 changed files with 206 additions and 2 deletions
|
|
@ -6960,11 +6960,26 @@ pub enum WorkspaceCommand {
|
|||
/// Display package metadata.
|
||||
#[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)]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
48
crates/uv/src/commands/workspace/dir.rs
Normal file
48
crates/uv/src/commands/workspace/dir.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use std::fmt::Write;
|
||||
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;
|
||||
use crate::printer::Printer;
|
||||
|
||||
/// Print the path to the workspace dir
|
||||
pub(crate) async fn dir(
|
||||
package_name: Option<PackageName>,
|
||||
project_dir: &Path,
|
||||
preview: Preview,
|
||||
printer: Printer,
|
||||
) -> 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.")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
writeln!(printer.stdout(), "{}", dir.simplified_display().cyan())?;
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
|
@ -1 +1,2 @@
|
|||
pub(crate) mod dir;
|
||||
pub(crate) mod metadata;
|
||||
|
|
|
|||
|
|
@ -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, printer).await
|
||||
}
|
||||
},
|
||||
Commands::BuildBackend { command } => spawn_blocking(move || match command {
|
||||
BuildBackendCommand::BuildSdist { sdist_directory } => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -141,4 +141,5 @@ mod workflow;
|
|||
|
||||
mod extract;
|
||||
mod workspace;
|
||||
mod workspace_dir;
|
||||
mod workspace_metadata;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
123
crates/uv/tests/it/workspace_dir.rs
Normal file
123
crates/uv/tests/it/workspace_dir.rs
Normal file
|
|
@ -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`
|
||||
#[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
|
||||
"###
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue