Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6968,6 +6968,11 @@ pub enum WorkspaceCommand {
/// 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),
/// List the members of a workspace.
///
/// Displays newline separated names of workspace members.
#[command(hide = true)]
List(WorkspaceListArgs),
}

#[derive(Args, Debug)]
Expand All @@ -6980,6 +6985,9 @@ pub struct WorkspaceDirArgs {
pub package: Option<PackageName>,
}

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

/// 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 @@ -24,6 +24,7 @@ bitflags::bitflags! {
const INIT_PROJECT_FLAG = 1 << 12;
const WORKSPACE_METADATA = 1 << 13;
const WORKSPACE_DIR = 1 << 14;
const WORKSPACE_LIST = 1 << 15;
}
}

Expand All @@ -48,6 +49,7 @@ impl PreviewFeatures {
Self::INIT_PROJECT_FLAG => "init-project-flag",
Self::WORKSPACE_METADATA => "workspace-metadata",
Self::WORKSPACE_DIR => "workspace-dir",
Self::WORKSPACE_LIST => "workspace-list",
_ => panic!("`flag_as_str` can only be used for exactly one feature flag"),
}
}
Expand Down Expand Up @@ -100,6 +102,7 @@ impl FromStr for PreviewFeatures {
"init-project-flag" => Self::INIT_PROJECT_FLAG,
"workspace-metadata" => Self::WORKSPACE_METADATA,
"workspace-dir" => Self::WORKSPACE_DIR,
"workspace-list" => Self::WORKSPACE_LIST,
_ => {
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 @@ -68,6 +68,7 @@ use uv_python::PythonEnvironment;
use uv_scripts::Pep723Script;
pub(crate) use venv::venv;
pub(crate) use workspace::dir::dir;
pub(crate) use workspace::list::list;
pub(crate) use workspace::metadata::metadata;

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

use anyhow::Result;

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

use crate::commands::ExitStatus;
use crate::printer::Printer;

/// List workspace members
pub(crate) async fn list(
project_dir: &Path,
preview: Preview,
printer: Printer,
) -> Result<ExitStatus> {
if !preview.is_enabled(PreviewFeatures::WORKSPACE_LIST) {
warn_user!(
"The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeatures::WORKSPACE_LIST
);
}

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

for name in workspace.packages().keys() {
writeln!(printer.stdout(), "{}", name.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,2 +1,3 @@
pub(crate) mod dir;
pub(crate) mod list;
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 @@ -1741,6 +1741,9 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
WorkspaceCommand::Dir(args) => {
commands::dir(args.package, &project_dir, globals.preview, printer).await
}
WorkspaceCommand::List(_args) => {
commands::list(&project_dir, globals.preview, printer).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 @@ -1080,6 +1080,14 @@ impl TestContext {
command
}

/// Create a `uv workspace list` command with options shared across scenarios.
pub fn workspace_list(&self) -> Command {
let mut command = Self::new_command();
command.arg("workspace").arg("list");
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 @@ -142,4 +142,5 @@ mod workflow;
mod extract;
mod workspace;
mod workspace_dir;
mod workspace_list;
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 | WORKSPACE_DIR,
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 | WORKSPACE_LIST,
),
},
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 | WORKSPACE_DIR,
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 | WORKSPACE_LIST,
),
},
python_preference: Managed,
Expand Down
220 changes: 220 additions & 0 deletions crates/uv/tests/it/workspace_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use assert_fs::fixture::PathChild;

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

/// Test basic list output for a simple workspace with one member.
#[test]
fn workspace_list_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_list().current_dir(&workspace), @r"
success: true
exit_code: 0
----- stdout -----
foo

----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
"
);
}

/// Test list output for a root workspace (workspace with a root package).
#[test]
fn workspace_list_root_workspace() -> Result<()> {
let context = TestContext::new("3.12");
let workspace = context.temp_dir.child("workspace");

copy_dir_ignore(
context
.workspace_root
.join("scripts/workspaces/albatross-root-workspace"),
&workspace,
)?;

uv_snapshot!(context.filters(), context.workspace_list().current_dir(&workspace), @r"
success: true
exit_code: 0
----- stdout -----
albatross
bird-feeder
seeds

----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
"
);

Ok(())
}

/// Test list output for a virtual workspace (no root package).
#[test]
fn workspace_list_virtual_workspace() -> Result<()> {
let context = TestContext::new("3.12");
let workspace = context.temp_dir.child("workspace");

copy_dir_ignore(
context
.workspace_root
.join("scripts/workspaces/albatross-virtual-workspace"),
&workspace,
)?;

uv_snapshot!(context.filters(), context.workspace_list().current_dir(&workspace), @r"
success: true
exit_code: 0
----- stdout -----
albatross
bird-feeder
seeds

----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
"
);

Ok(())
}

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

copy_dir_ignore(
context
.workspace_root
.join("scripts/workspaces/albatross-root-workspace"),
&workspace,
)?;

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

uv_snapshot!(context.filters(), context.workspace_list().current_dir(&member_dir), @r"
success: true
exit_code: 0
----- stdout -----
albatross
bird-feeder
seeds

----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
"
);

Ok(())
}

/// Test list output for a workspace with multiple packages.
#[test]
fn workspace_list_multiple_members() {
let context = TestContext::new("3.12");

// Initialize workspace root
context.init().arg("pkg-a").assert().success();

let workspace_root = context.temp_dir.child("pkg-a");

// Add more members
context
.init()
.arg("pkg-b")
.current_dir(&workspace_root)
.assert()
.success();

context
.init()
.arg("pkg-c")
.current_dir(&workspace_root)
.assert()
.success();

uv_snapshot!(context.filters(), context.workspace_list().current_dir(&workspace_root), @r"
success: true
exit_code: 0
----- stdout -----
pkg-a
pkg-b
pkg-c

----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
"
);
}

/// Test list output for a single project (not a workspace).
#[test]
fn workspace_list_single_project() {
let context = TestContext::new("3.12");

context.init().arg("my-project").assert().success();

let project = context.temp_dir.child("my-project");

uv_snapshot!(context.filters(), context.workspace_list().current_dir(&project), @r"
success: true
exit_code: 0
----- stdout -----
my-project

----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
"
);
}

/// Test list output with excluded packages.
#[test]
fn workspace_list_with_excluded() -> Result<()> {
let context = TestContext::new("3.12");
let workspace = context.temp_dir.child("workspace");

copy_dir_ignore(
context
.workspace_root
.join("scripts/workspaces/albatross-project-in-excluded"),
&workspace,
)?;

uv_snapshot!(context.filters(), context.workspace_list().current_dir(&workspace), @r"
success: true
exit_code: 0
----- stdout -----
albatross

----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
"
);

Ok(())
}

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

uv_snapshot!(context.filters(), context.workspace_list(), @r"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
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 @@ -74,6 +74,7 @@ The following preview features are available:
[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`.
- `workspace-list`: Allows using `uv workspace list`.

## Disabling preview features

Expand Down
Loading