use std::collections::BTreeMap;
use std::env;
use std::path::PathBuf;
use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use assert_fs::fixture::{FileWriteStr, PathChild};
use assert_fs::prelude::FileTouch;
use indoc::indoc;
use insta::{assert_json_snapshot, assert_snapshot};
use serde::{Deserialize, Serialize};
use uv_test::{copy_dir_ignore, make_project, uv_snapshot};
fn workspaces_dir() -> PathBuf {
env::current_dir()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap()
.join("test")
.join("workspaces")
}
#[test]
#[cfg(feature = "test-pypi")]
fn test_albatross_in_examples_bird_feeder() {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
copy_dir_ignore(workspaces_dir(), &workspace).unwrap();
let current_dir = workspace
.join("albatross-in-example")
.join("examples")
.join("bird-feeder");
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ bird-feeder==1.0.0 (from file://[TEMP_DIR]/workspace/albatross-in-example/examples/bird-feeder)
+ iniconfig==2.0.0
"
);
context.assert_file(current_dir.join("check_installed_bird_feeder.py"));
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Checked 2 packages in [TIME]
"
);
context.assert_file(current_dir.join("check_installed_bird_feeder.py"));
}
#[test]
#[cfg(feature = "test-pypi")]
fn test_albatross_in_examples() {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
copy_dir_ignore(workspaces_dir(), &workspace).unwrap();
let current_dir = workspace.join("albatross-in-example");
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ albatross==0.1.0 (from file://[TEMP_DIR]/workspace/albatross-in-example)
+ iniconfig==2.0.0
"
);
context.assert_file(current_dir.join("check_installed_albatross.py"));
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Checked 2 packages in [TIME]
"
);
context.assert_file(current_dir.join("check_installed_albatross.py"));
}
#[test]
#[cfg(feature = "test-pypi")]
fn test_albatross_just_project() {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
copy_dir_ignore(workspaces_dir(), &workspace).unwrap();
let current_dir = workspace.join("albatross-just-project");
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ albatross==0.1.0 (from file://[TEMP_DIR]/workspace/albatross-just-project)
+ iniconfig==2.0.0
"
);
context.assert_file(current_dir.join("check_installed_albatross.py"));
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Checked 2 packages in [TIME]
"
);
context.assert_file(current_dir.join("check_installed_albatross.py"));
}
#[test]
#[cfg(feature = "test-pypi")]
fn test_albatross_project_in_excluded() {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
copy_dir_ignore(workspaces_dir(), &workspace).unwrap();
let current_dir = workspace.join("albatross-project-in-excluded");
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ albatross==0.1.0 (from file://[TEMP_DIR]/workspace/albatross-project-in-excluded)
+ iniconfig==2.0.0
"
);
let current_dir = workspace
.join("albatross-project-in-excluded")
.join("excluded")
.join("bird-feeder");
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 2 packages in [TIME]
+ bird-feeder==1.0.0 (from file://[TEMP_DIR]/workspace/albatross-project-in-excluded/excluded/bird-feeder)
+ iniconfig==2.0.0
"
);
context.assert_file(current_dir.join("check_installed_bird_feeder.py"));
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Checked 2 packages in [TIME]
"
);
context.assert_file(current_dir.join("check_installed_bird_feeder.py"));
let current_dir = workspace
.join("albatross-project-in-excluded")
.join("packages")
.join("seeds");
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: The project is marked as unmanaged: [TEMP_DIR]/workspace/albatross-project-in-excluded/packages/seeds
"
);
}
#[test]
#[cfg(feature = "test-pypi")]
fn test_albatross_root_workspace() {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
copy_dir_ignore(workspaces_dir(), &workspace).unwrap();
let current_dir = workspace.join("albatross-root-workspace");
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 5 packages in [TIME]
Prepared 5 packages in [TIME]
Installed 5 packages in [TIME]
+ albatross==0.1.0 (from file://[TEMP_DIR]/workspace/albatross-root-workspace)
+ bird-feeder==1.0.0 (from file://[TEMP_DIR]/workspace/albatross-root-workspace/packages/bird-feeder)
+ idna==3.6
+ iniconfig==2.0.0
+ seeds==1.0.0 (from file://[TEMP_DIR]/workspace/albatross-root-workspace/packages/seeds)
"
);
context.assert_file(current_dir.join("check_installed_albatross.py"));
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 5 packages in [TIME]
Checked 5 packages in [TIME]
"
);
context.assert_file(current_dir.join("check_installed_albatross.py"));
}
#[test]
#[cfg(feature = "test-pypi")]
fn test_albatross_root_workspace_bird_feeder() {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
copy_dir_ignore(workspaces_dir(), &workspace).unwrap();
let current_dir = workspace
.join("albatross-root-workspace")
.join("packages")
.join("bird-feeder");
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: [TEMP_DIR]/workspace/albatross-root-workspace/.venv
Resolved 5 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ bird-feeder==1.0.0 (from file://[TEMP_DIR]/workspace/albatross-root-workspace/packages/bird-feeder)
+ idna==3.6
+ iniconfig==2.0.0
+ seeds==1.0.0 (from file://[TEMP_DIR]/workspace/albatross-root-workspace/packages/seeds)
"
);
context.assert_file(current_dir.join("check_installed_bird_feeder.py"));
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 5 packages in [TIME]
Checked 4 packages in [TIME]
"
);
context.assert_file(current_dir.join("check_installed_bird_feeder.py"));
}
#[test]
#[cfg(feature = "test-pypi")]
fn test_albatross_root_workspace_albatross() {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
copy_dir_ignore(workspaces_dir(), &workspace).unwrap();
let current_dir = workspace
.join("albatross-root-workspace")
.join("packages")
.join("bird-feeder");
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: [TEMP_DIR]/workspace/albatross-root-workspace/.venv
Resolved 5 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ bird-feeder==1.0.0 (from file://[TEMP_DIR]/workspace/albatross-root-workspace/packages/bird-feeder)
+ idna==3.6
+ iniconfig==2.0.0
+ seeds==1.0.0 (from file://[TEMP_DIR]/workspace/albatross-root-workspace/packages/seeds)
"
);
context.assert_file(current_dir.join("check_installed_albatross.py"));
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 5 packages in [TIME]
Checked 4 packages in [TIME]
"
);
context.assert_file(current_dir.join("check_installed_albatross.py"));
}
#[test]
#[cfg(feature = "test-pypi")]
fn test_albatross_virtual_workspace() {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
copy_dir_ignore(workspaces_dir(), &workspace).unwrap();
let current_dir = workspace
.join("albatross-virtual-workspace")
.join("packages")
.join("bird-feeder");
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: [TEMP_DIR]/workspace/albatross-virtual-workspace/.venv
Resolved 7 packages in [TIME]
Prepared 5 packages in [TIME]
Installed 5 packages in [TIME]
+ anyio==4.3.0
+ bird-feeder==1.0.0 (from file://[TEMP_DIR]/workspace/albatross-virtual-workspace/packages/bird-feeder)
+ idna==3.6
+ seeds==1.0.0 (from file://[TEMP_DIR]/workspace/albatross-virtual-workspace/packages/seeds)
+ sniffio==1.3.1
"
);
context.assert_file(current_dir.join("check_installed_bird_feeder.py"));
uv_snapshot!(context.filters(), context.sync().current_dir(¤t_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 7 packages in [TIME]
Checked 5 packages in [TIME]
"
);
context.assert_file(current_dir.join("check_installed_bird_feeder.py"));
}
#[test]
#[cfg(feature = "test-pypi")]
fn test_uv_run_with_package_virtual_workspace() -> Result<()> {
let context = uv_test::test_context!("3.12");
let work_dir = context.temp_dir.join("albatross-virtual-workspace");
copy_dir_ignore(
workspaces_dir().join("albatross-virtual-workspace"),
&work_dir,
)?;
let mut filters = context.filters();
filters.push((
r"Using Python 3.12.\[X\] interpreter at: .*",
"Using Python 3.12.[X] interpreter at: [PYTHON]",
));
uv_snapshot!(filters, context
.run()
.arg("--package")
.arg("bird-feeder")
.arg("packages/bird-feeder/check_installed_bird_feeder.py")
.current_dir(&work_dir), @"
success: true
exit_code: 0
----- stdout -----
Success
----- stderr -----
warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 7 packages in [TIME]
Prepared 5 packages in [TIME]
Installed 5 packages in [TIME]
+ anyio==4.3.0
+ bird-feeder==1.0.0 (from file://[TEMP_DIR]/albatross-virtual-workspace/packages/bird-feeder)
+ idna==3.6
+ seeds==1.0.0 (from file://[TEMP_DIR]/albatross-virtual-workspace/packages/seeds)
+ sniffio==1.3.1
"
);
uv_snapshot!(context.filters(), context
.run()
.arg("--package")
.arg("albatross")
.arg("packages/albatross/check_installed_albatross.py")
.current_dir(&work_dir), @"
success: true
exit_code: 0
----- stdout -----
Success
----- stderr -----
warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
Resolved 7 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ albatross==0.1.0 (from file://[TEMP_DIR]/albatross-virtual-workspace/packages/albatross)
+ iniconfig==2.0.0
"
);
Ok(())
}
#[test]
#[cfg(feature = "test-pypi")]
fn test_uv_run_virtual_workspace_root() -> Result<()> {
let context = uv_test::test_context!("3.12");
let work_dir = context.temp_dir.join("albatross-virtual-workspace");
copy_dir_ignore(
workspaces_dir().join("albatross-virtual-workspace"),
&work_dir,
)?;
uv_snapshot!(context.filters(), context
.run()
.arg("packages/albatross/check_installed_albatross.py")
.current_dir(&work_dir), @"
success: true
exit_code: 0
----- stdout -----
Success
----- stderr -----
warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 7 packages in [TIME]
Prepared 7 packages in [TIME]
Installed 7 packages in [TIME]
+ albatross==0.1.0 (from file://[TEMP_DIR]/albatross-virtual-workspace/packages/albatross)
+ anyio==4.3.0
+ bird-feeder==1.0.0 (from file://[TEMP_DIR]/albatross-virtual-workspace/packages/bird-feeder)
+ idna==3.6
+ iniconfig==2.0.0
+ seeds==1.0.0 (from file://[TEMP_DIR]/albatross-virtual-workspace/packages/seeds)
+ sniffio==1.3.1
"
);
Ok(())
}
#[test]
#[cfg(feature = "test-pypi")]
fn test_uv_run_with_package_root_workspace() -> Result<()> {
let context = uv_test::test_context!("3.12");
let work_dir = context.temp_dir.join("albatross-root-workspace");
copy_dir_ignore(workspaces_dir().join("albatross-root-workspace"), &work_dir)?;
let mut filters = context.filters();
filters.push((
r"Using Python 3.12.\[X\] interpreter at: .*",
"Using Python 3.12.[X] interpreter at: [PYTHON]",
));
uv_snapshot!(filters, context
.run()
.arg("--package")
.arg("bird-feeder")
.arg("packages/bird-feeder/check_installed_bird_feeder.py")
.current_dir(&work_dir), @"
success: true
exit_code: 0
----- stdout -----
Success
----- stderr -----
warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 5 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ bird-feeder==1.0.0 (from file://[TEMP_DIR]/albatross-root-workspace/packages/bird-feeder)
+ idna==3.6
+ iniconfig==2.0.0
+ seeds==1.0.0 (from file://[TEMP_DIR]/albatross-root-workspace/packages/seeds)
"
);
uv_snapshot!(context.filters(), context
.run()
.arg("--package")
.arg("albatross")
.arg("check_installed_albatross.py")
.current_dir(&work_dir), @"
success: true
exit_code: 0
----- stdout -----
Success
----- stderr -----
warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
Resolved 5 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ albatross==0.1.0 (from file://[TEMP_DIR]/albatross-root-workspace)
"
);
Ok(())
}
#[test]
#[cfg(feature = "test-pypi")]
fn test_uv_run_isolate() -> Result<()> {
let context = uv_test::test_context!("3.12");
let work_dir = context.temp_dir.join("albatross-root-workspace");
copy_dir_ignore(workspaces_dir().join("albatross-root-workspace"), &work_dir)?;
let mut filters = context.filters();
filters.push((
r"Using Python 3.12.\[X\] interpreter at: .*",
"Using Python 3.12.[X] interpreter at: [PYTHON]",
));
uv_snapshot!(context.filters(), context
.run()
.arg("--package")
.arg("albatross")
.arg("check_installed_albatross.py")
.current_dir(&work_dir), @"
success: true
exit_code: 0
----- stdout -----
Success
----- stderr -----
warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 5 packages in [TIME]
Prepared 5 packages in [TIME]
Installed 5 packages in [TIME]
+ albatross==0.1.0 (from file://[TEMP_DIR]/albatross-root-workspace)
+ bird-feeder==1.0.0 (from file://[TEMP_DIR]/albatross-root-workspace/packages/bird-feeder)
+ idna==3.6
+ iniconfig==2.0.0
+ seeds==1.0.0 (from file://[TEMP_DIR]/albatross-root-workspace/packages/seeds)
"
);
uv_snapshot!(filters, context
.run()
.arg("--package")
.arg("bird-feeder")
.arg("check_installed_albatross.py")
.current_dir(&work_dir), @"
success: true
exit_code: 0
----- stdout -----
Success
----- stderr -----
warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
Resolved 5 packages in [TIME]
Checked 4 packages in [TIME]
"
);
uv_snapshot!(filters, context
.run()
.arg("--isolated")
.arg("--package")
.arg("bird-feeder")
.arg("check_installed_albatross.py")
.current_dir(&work_dir), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Resolved 5 packages in [TIME]
Installed 4 packages in [TIME]
+ bird-feeder==1.0.0 (from file://[TEMP_DIR]/albatross-root-workspace/packages/bird-feeder)
+ idna==3.6
+ iniconfig==2.0.0
+ seeds==1.0.0 (from file://[TEMP_DIR]/albatross-root-workspace/packages/seeds)
Traceback (most recent call last):
File "[TEMP_DIR]/albatross-root-workspace/check_installed_albatross.py", line 1, in <module>
from albatross import fly
ModuleNotFoundError: No module named 'albatross'
"#
);
Ok(())
}
fn workspace_lock_idempotence(workspace: &str, subdirectories: &[&str]) -> Result<()> {
let mut shared_lock = None;
for dir in subdirectories {
let context = uv_test::test_context!("3.12");
let work_dir = context.temp_dir.join(workspace);
copy_dir_ignore(workspaces_dir().join(workspace), &work_dir)?;
context
.lock()
.current_dir(work_dir.join(dir))
.assert()
.success();
let lock = fs_err::read_to_string(work_dir.join("uv.lock"))?;
if let Some(shared_lock) = &shared_lock {
assert_eq!(shared_lock, &lock);
} else {
shared_lock = Some(lock);
}
}
Ok(())
}
#[test]
#[cfg(feature = "test-pypi")]
fn workspace_lock_idempotence_root_workspace() -> Result<()> {
workspace_lock_idempotence(
"albatross-root-workspace",
&[".", "packages/bird-feeder", "packages/seeds"],
)?;
Ok(())
}
#[test]
#[cfg(feature = "test-pypi")]
fn workspace_lock_idempotence_virtual_workspace() -> Result<()> {
workspace_lock_idempotence(
"albatross-virtual-workspace",
&[
".",
"packages/albatross",
"packages/bird-feeder",
"packages/seeds",
],
)?;
Ok(())
}
#[derive(Deserialize, Serialize, Debug, PartialEq)]
struct SourceLock {
package: Vec<Package>,
}
impl SourceLock {
fn sources(&self) -> BTreeMap<String, toml::Value> {
self.package
.iter()
.map(|package| (package.name.clone(), package.source.clone()))
.collect()
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq)]
struct Package {
name: String,
source: toml::Value,
}
#[test]
fn workspace_to_workspace_paths_dependencies() -> Result<()> {
let context = uv_test::test_context!("3.12");
let main_workspace = context.temp_dir.child("main-workspace");
main_workspace
.child("pyproject.toml")
.write_str(indoc! {r#"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
let deps = indoc! {r#"
dependencies = ["b"]
[tool.uv.sources]
b = { workspace = true }
"#};
make_project(&main_workspace.join("packages").join("a"), "a", deps)?;
let deps = indoc! {r#"
dependencies = ["c"]
[tool.uv.sources]
c = { path = "../../../other-workspace/packages/c", editable = true }
"#};
make_project(&main_workspace.join("packages").join("b"), "b", deps)?;
let other_workspace = context.temp_dir.child("other-workspace");
other_workspace
.child("pyproject.toml")
.write_str(indoc! {r#"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
let deps = indoc! {r#"
dependencies = ["d"]
[tool.uv.sources]
d = { workspace = true }
"#};
make_project(&other_workspace.join("packages").join("c"), "c", deps)?;
let deps = indoc! {r"
dependencies = []
"};
make_project(&other_workspace.join("packages").join("d"), "d", deps)?;
let deps = indoc! {r#"
dependencies = ["numpy>=2.0.0,<3"]
"#};
make_project(&other_workspace.join("packages").join("e"), "e", deps)?;
uv_snapshot!(context.filters(), context.lock().current_dir(&main_workspace), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 4 packages in [TIME]
"
);
let lock: SourceLock =
toml::from_str(&fs_err::read_to_string(main_workspace.join("uv.lock"))?)?;
assert_json_snapshot!(lock.sources(), @r#"
{
"a": {
"editable": "packages/a"
},
"b": {
"editable": "packages/b"
},
"c": {
"editable": "../other-workspace/packages/c"
},
"d": {
"editable": "../other-workspace/packages/d"
}
}
"#);
Ok(())
}
#[test]
fn workspace_empty_member() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
let deps = indoc! {r#"
dependencies = ["b"]
[tool.uv.sources]
b = { workspace = true }
"#};
make_project(&workspace.join("packages").join("a"), "a", deps)?;
let deps = indoc! {r"
"};
make_project(&workspace.join("packages").join("b"), "b", deps)?;
fs_err::create_dir_all(workspace.join("packages").join("c"))?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 2 packages in [TIME]
"
);
Ok(())
}
#[test]
fn workspace_gitignored_member() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
workspace.child(".gitignore").write_str("__pycache__/\n")?;
let deps = indoc! {r#"
dependencies = ["b"]
[tool.uv.sources]
b = { workspace = true }
"#};
make_project(&workspace.join("packages").join("a"), "a", deps)?;
let deps = indoc! {r"
"};
make_project(&workspace.join("packages").join("b"), "b", deps)?;
fs_err::create_dir_all(workspace.join("packages").join("c").join("__pycache__"))?;
fs_err::write(
workspace
.join("packages")
.join("c")
.join("__pycache__")
.join("test.cpython-312.pyc"),
"fake",
)?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 2 packages in [TIME]
"
);
let lock: SourceLock = toml::from_str(&fs_err::read_to_string(workspace.join("uv.lock"))?)?;
assert_json_snapshot!(lock.sources(), @r#"
{
"a": {
"editable": "packages/a"
},
"b": {
"editable": "packages/b"
}
}
"#);
Ok(())
}
#[test]
fn workspace_gitignored_member_in_subdirectory() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
workspace.child(".gitignore").write_str("__pycache__/\n")?;
let deps = indoc! {r#"
dependencies = ["b"]
[tool.uv.sources]
b = { workspace = true }
"#};
make_project(&workspace.join("packages").join("a"), "a", deps)?;
let deps = indoc! {r"
"};
make_project(&workspace.join("packages").join("b"), "b", deps)?;
fs_err::create_dir_all(
workspace
.join("packages")
.join("c")
.join("foo")
.join("__pycache__"),
)?;
fs_err::write(
workspace
.join("packages")
.join("c")
.join("foo")
.join("__pycache__")
.join("test.cpython-312.pyc"),
"fake",
)?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 2 packages in [TIME]
"
);
let lock: SourceLock = toml::from_str(&fs_err::read_to_string(workspace.join("uv.lock"))?)?;
assert_json_snapshot!(lock.sources(), @r#"
{
"a": {
"editable": "packages/a"
},
"b": {
"editable": "packages/b"
}
}
"#);
Ok(())
}
#[test]
fn workspace_ignored_member() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
workspace.child(".ignore").write_str("__pycache__/\n")?;
let deps = indoc! {r#"
dependencies = ["b"]
[tool.uv.sources]
b = { workspace = true }
"#};
make_project(&workspace.join("packages").join("a"), "a", deps)?;
let deps = indoc! {r"
"};
make_project(&workspace.join("packages").join("b"), "b", deps)?;
fs_err::create_dir_all(workspace.join("packages").join("c").join("__pycache__"))?;
fs_err::write(
workspace
.join("packages")
.join("c")
.join("__pycache__")
.join("test.cpython-312.pyc"),
"fake",
)?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 2 packages in [TIME]
"
);
let lock: SourceLock = toml::from_str(&fs_err::read_to_string(workspace.join("uv.lock"))?)?;
assert_json_snapshot!(lock.sources(), @r#"
{
"a": {
"editable": "packages/a"
},
"b": {
"editable": "packages/b"
}
}
"#);
Ok(())
}
#[test]
fn workspace_nonempty_member_no_pyproject() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
workspace.child(".gitignore").write_str("__pycache__/\n")?;
let deps = indoc! {r#"
dependencies = ["b"]
[tool.uv.sources]
b = { workspace = true }
"#};
make_project(&workspace.join("packages").join("a"), "a", deps)?;
let deps = indoc! {r"
"};
make_project(&workspace.join("packages").join("b"), "b", deps)?;
fs_err::create_dir_all(workspace.join("packages").join("c"))?;
fs_err::write(
workspace.join("packages").join("c").join("README.md"),
"# Hello",
)?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Workspace member `[TEMP_DIR]/workspace/packages/c` is missing a `pyproject.toml` (matches: `packages/*`)
"
);
Ok(())
}
#[test]
fn workspace_hidden_files() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
let deps = indoc! {r#"
dependencies = ["b"]
[tool.uv.sources]
b = { workspace = true }
"#};
make_project(&workspace.join("packages").join("a"), "a", deps)?;
let deps = indoc! {r"
"};
make_project(&workspace.join("packages").join("b"), "b", deps)?;
fs_err::create_dir_all(workspace.join("packages").join(".c"))?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 2 packages in [TIME]
"
);
let lock: SourceLock = toml::from_str(&fs_err::read_to_string(workspace.join("uv.lock"))?)?;
assert_json_snapshot!(lock.sources(), @r#"
{
"a": {
"editable": "packages/a"
},
"b": {
"editable": "packages/b"
}
}
"#);
Ok(())
}
#[test]
fn workspace_hidden_member() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
let deps = indoc! {r#"
dependencies = ["b"]
[tool.uv.sources]
b = { workspace = true }
"#};
make_project(&workspace.join("packages").join("a"), "a", deps)?;
let deps = indoc! {r#"
dependencies = ["c"]
[tool.uv.sources]
c = { workspace = true }
"#};
make_project(&workspace.join("packages").join("b"), "b", deps)?;
let deps = indoc! {r"
dependencies = []
"};
make_project(&workspace.join("packages").join(".c"), "c", deps)?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 3 packages in [TIME]
"
);
let lock: SourceLock = toml::from_str(&fs_err::read_to_string(workspace.join("uv.lock"))?)?;
assert_json_snapshot!(lock.sources(), @r#"
{
"a": {
"editable": "packages/a"
},
"b": {
"editable": "packages/b"
},
"c": {
"editable": "packages/.c"
}
}
"#);
Ok(())
}
#[test]
fn workspace_non_included_member() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
let deps = indoc! {r#"
dependencies = ["b"]
[tool.uv.sources]
b = { workspace = true }
"#};
make_project(&workspace.join("packages").join("a"), "a", deps)?;
let deps = indoc! {r"
dependencies = []
"};
make_project(&workspace.join("packages").join("b"), "b", deps)?;
let deps = indoc! {r"
dependencies = []
"};
make_project(&workspace.join("c"), "c", deps)?;
uv_snapshot!(context.filters(), context.lock().current_dir(workspace.join("c")), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 1 package in [TIME]
"
);
let lock: SourceLock = toml::from_str(&fs_err::read_to_string(
workspace.join("c").join("uv.lock"),
)?)?;
assert_json_snapshot!(lock.sources(), @r#"
{
"c": {
"editable": "."
}
}
"#);
Ok(())
}
#[test]
fn workspace_inherit_sources() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "workspace"
version = "0.1.0"
dependencies = []
requires-python = ">=3.12"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
workspace.child("src/__init__.py").touch()?;
let leaf = workspace.child("packages").child("leaf");
leaf.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "leaf"
version = "0.1.0"
dependencies = ["library"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
leaf.child("src/__init__.py").touch()?;
let library = context.temp_dir.child("library");
library.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "library"
version = "0.1.0"
dependencies = []
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
library.child("src/__init__.py").touch()?;
uv_snapshot!(context.filters(), context.lock().arg("--offline").current_dir(&workspace), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
× No solution found when resolving dependencies:
╰─▶ Because library was not found in the cache and leaf depends on library, we can conclude that leaf's requirements are unsatisfiable.
And because your workspace requires leaf, we can conclude that your workspace's requirements are unsatisfiable.
hint: Packages were unavailable because the network was disabled. When the network is disabled, registry packages may only be read from the cache.
"
);
leaf.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "leaf"
version = "0.1.0"
dependencies = ["library"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.sources]
library = { path = "../../../library", editable = true }
"#})?;
leaf.child("src/__init__.py").touch()?;
uv_snapshot!(context.filters(), context.lock().arg("--offline").current_dir(&workspace), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 3 packages in [TIME]
"
);
leaf.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "leaf"
version = "0.1.0"
dependencies = ["library"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
workspace.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "workspace"
version = "0.1.0"
dependencies = []
requires-python = ">=3.12"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.sources]
library = { path = "../library", editable = true }
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
uv_snapshot!(context.filters(), context.lock().arg("--offline").current_dir(&workspace), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 3 packages in [TIME]
"
);
let lock = fs_err::read_to_string(workspace.join("uv.lock")).unwrap();
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r#"
version = 1
revision = 3
requires-python = ">=3.12"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[manifest]
members = [
"leaf",
"workspace",
]
[[package]]
name = "leaf"
version = "0.1.0"
source = { editable = "packages/leaf" }
dependencies = [
{ name = "library" },
]
[package.metadata]
requires-dist = [{ name = "library", editable = "../library" }]
[[package]]
name = "library"
version = "0.1.0"
source = { editable = "../library" }
[[package]]
name = "workspace"
version = "0.1.0"
source = { editable = "." }
"#
);
});
workspace.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "workspace"
version = "0.1.0"
dependencies = []
requires-python = ">=3.12"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.sources]
library = { path = "../library", editable = true }
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
leaf.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "leaf"
version = "0.1.0"
dependencies = ["library"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.sources]
application = { path = "../application", editable = true }
"#})?;
uv_snapshot!(context.filters(), context.lock().arg("--offline").current_dir(&workspace), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 3 packages in [TIME]
"
);
Ok(())
}
#[test]
#[cfg(feature = "test-pypi")]
fn workspace_unsatisfiable_member_dependencies() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "workspace"
version = "0.1.0"
dependencies = []
requires-python = ">=3.12"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
workspace.child("src/__init__.py").touch()?;
let leaf = workspace.child("packages").child("leaf");
leaf.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "leaf"
version = "0.1.0"
dependencies = ["httpx>9999"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
leaf.child("src/__init__.py").touch()?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
× No solution found when resolving dependencies:
╰─▶ Because only httpx<=0.27.0 is available and leaf depends on httpx>9999, we can conclude that leaf's requirements are unsatisfiable.
And because your workspace requires leaf, we can conclude that your workspace's requirements are unsatisfiable.
"
);
Ok(())
}
#[test]
#[cfg(feature = "test-pypi")]
fn workspace_unsatisfiable_member_dependencies_conflicting() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "workspace"
version = "0.1.0"
dependencies = []
requires-python = ">=3.12"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
workspace.child("src/__init__.py").touch()?;
let foo = workspace.child("packages").child("foo");
foo.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "foo"
version = "0.1.0"
dependencies = ["anyio==4.1.0"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
foo.child("src/__init__.py").touch()?;
let bar = workspace.child("packages").child("bar");
bar.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "bar"
version = "0.1.0"
dependencies = ["anyio==4.2.0"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
bar.child("src/__init__.py").touch()?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
× No solution found when resolving dependencies:
╰─▶ Because bar depends on anyio==4.2.0 and foo depends on anyio==4.1.0, we can conclude that bar and foo are incompatible.
And because your workspace requires bar and foo, we can conclude that your workspace's requirements are unsatisfiable.
"
);
Ok(())
}
#[test]
#[cfg(feature = "test-pypi")]
fn workspace_unsatisfiable_member_dependencies_conflicting_threeway() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "workspace"
version = "0.1.0"
dependencies = []
requires-python = ">=3.12"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
workspace.child("src/__init__.py").touch()?;
let red = workspace.child("packages").child("red");
red.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "red"
version = "0.1.0"
dependencies = ["anyio==4.1.0"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
red.child("src/__init__.py").touch()?;
let knot = workspace.child("packages").child("knot");
knot.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "knot"
version = "0.1.0"
dependencies = ["anyio==4.2.0"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
knot.child("src/__init__.py").touch()?;
let bird = workspace.child("packages").child("bird");
bird.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "bird"
version = "0.1.0"
dependencies = ["anyio==4.3.0"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
bird.child("src/__init__.py").touch()?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
× No solution found when resolving dependencies:
╰─▶ Because bird depends on anyio==4.3.0 and knot depends on anyio==4.2.0, we can conclude that bird and knot are incompatible.
And because your workspace requires bird and knot, we can conclude that your workspace's requirements are unsatisfiable.
"
);
Ok(())
}
#[test]
#[cfg(feature = "test-pypi")]
fn workspace_unsatisfiable_member_dependencies_conflicting_extra() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "workspace"
version = "0.1.0"
dependencies = []
requires-python = ">=3.12"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
workspace.child("src/__init__.py").touch()?;
let foo = workspace.child("packages").child("foo");
foo.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "foo"
version = "0.1.0"
dependencies = ["anyio==4.1.0"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
foo.child("src/__init__.py").touch()?;
let bar = workspace.child("packages").child("bar");
bar.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "bar"
version = "0.1.0"
[project.optional-dependencies]
some_extra = ["anyio==4.2.0"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
bar.child("src/__init__.py").touch()?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
× No solution found when resolving dependencies:
╰─▶ Because bar[some-extra] depends on anyio==4.2.0 and foo depends on anyio==4.1.0, we can conclude that foo and bar[some-extra] are incompatible.
And because your workspace requires bar[some-extra] and foo, we can conclude that your workspace's requirements are unsatisfiable.
"
);
Ok(())
}
#[test]
#[cfg(feature = "test-pypi")]
fn workspace_unsatisfiable_member_dependencies_conflicting_dev() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "workspace"
version = "0.1.0"
dependencies = []
requires-python = ">=3.12"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
workspace.child("src/__init__.py").touch()?;
let foo = workspace.child("packages").child("foo");
foo.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "foo"
version = "0.1.0"
dependencies = ["anyio==4.1.0"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
foo.child("src/__init__.py").touch()?;
let bar = workspace.child("packages").child("bar");
bar.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "bar"
version = "0.1.0"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv]
dev-dependencies = ["anyio==4.2.0"]
"#})?;
bar.child("src/__init__.py").touch()?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: The `tool.uv.dev-dependencies` field (used in `packages/bar/pyproject.toml`) is deprecated and will be removed in a future release; use `dependency-groups.dev` instead
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
× No solution found when resolving dependencies:
╰─▶ Because bar:dev depends on anyio==4.2.0 and foo depends on anyio==4.1.0, we can conclude that foo and bar:dev are incompatible.
And because your workspace requires bar:dev and foo, we can conclude that your workspace's requirements are unsatisfiable.
"
);
Ok(())
}
#[test]
#[cfg(feature = "test-pypi")]
fn workspace_member_name_shadows_dependencies() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "workspace"
version = "0.1.0"
dependencies = []
requires-python = ">=3.12"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
workspace.child("src/__init__.py").touch()?;
let foo = workspace.child("packages").child("foo");
foo.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "foo"
version = "0.1.0"
dependencies = ["anyio==4.1.0"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
foo.child("src/__init__.py").touch()?;
let anyio = workspace.child("packages").child("anyio");
anyio.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "anyio"
version = "0.1.0"
dependencies = []
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
anyio.child("src/__init__.py").touch()?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
× Failed to build `foo @ file://[TEMP_DIR]/workspace/packages/foo`
├─▶ Failed to parse entry: `anyio`
╰─▶ `anyio` is included as a workspace member, but is missing an entry in `tool.uv.sources` (e.g., `anyio = { workspace = true }`)
"
);
Ok(())
}
#[test]
fn test_path_hopping() -> Result<()> {
let context = uv_test::test_context!("3.12");
let deps = indoc! {r#"
dependencies = ["foo"]
[tool.uv.sources]
foo = { path = "../libs/foo", editable = true }
"#};
let main_project_dir = context.temp_dir.join("project");
make_project(&main_project_dir, "project", deps)?;
let deps = indoc! {r#"
dependencies = ["bar"]
[tool.uv.sources]
bar = { path = "../../libs/bar", editable = true }
"#};
make_project(&context.temp_dir.join("libs").join("foo"), "foo", deps)?;
make_project(&context.temp_dir.join("libs").join("bar"), "bar", "")?;
uv_snapshot!(context.filters(), context.lock().arg("--preview").current_dir(&main_project_dir), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 3 packages in [TIME]
"
);
let lock: SourceLock =
toml::from_str(&fs_err::read_to_string(main_project_dir.join("uv.lock"))?)?;
assert_json_snapshot!(lock.sources(), @r#"
{
"bar": {
"editable": "../libs/bar"
},
"foo": {
"editable": "../libs/foo"
},
"project": {
"editable": "."
}
}
"#);
Ok(())
}
#[test]
#[cfg(feature = "test-git")]
fn transitive_dep_in_git_workspace_no_root() -> Result<()> {
let context = uv_test::test_context!("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "a"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["c"]
[tool.uv.sources]
c = { git = "https://github.com/astral-sh/workspace-virtual-root-test", subdirectory = "packages/c", rev = "fac39c8d4c5d0ef32744e2bb309bbe34a759fd46" }
"#
)?;
context.lock().assert().success();
let lock1: SourceLock =
toml::from_str(&fs_err::read_to_string(context.temp_dir.child("uv.lock"))?)?;
assert_json_snapshot!(lock1.sources(), @r#"
{
"a": {
"virtual": "."
},
"anyio": {
"registry": "https://pypi.org/simple"
},
"c": {
"git": "https://github.com/astral-sh/workspace-virtual-root-test?subdirectory=packages%2Fc&rev=fac39c8d4c5d0ef32744e2bb309bbe34a759fd46#fac39c8d4c5d0ef32744e2bb309bbe34a759fd46"
},
"d": {
"git": "https://github.com/astral-sh/workspace-virtual-root-test?subdirectory=packages%2Fd&rev=fac39c8d4c5d0ef32744e2bb309bbe34a759fd46#fac39c8d4c5d0ef32744e2bb309bbe34a759fd46"
},
"idna": {
"registry": "https://pypi.org/simple"
},
"sniffio": {
"registry": "https://pypi.org/simple"
}
}
"#);
pyproject_toml.write_str(
r#"
[project]
name = "a"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["c", "d"]
[tool.uv.sources]
c = { git = "https://github.com/astral-sh/workspace-virtual-root-test", subdirectory = "packages/c", rev = "fac39c8d4c5d0ef32744e2bb309bbe34a759fd46" }
d = { git = "https://github.com/astral-sh/workspace-virtual-root-test", subdirectory = "packages/d", rev = "fac39c8d4c5d0ef32744e2bb309bbe34a759fd46" }
"#
)?;
context.lock().assert().success();
let lock2: SourceLock =
toml::from_str(&fs_err::read_to_string(context.temp_dir.child("uv.lock"))?)?;
assert_eq!(lock1, lock2, "sources changed");
Ok(())
}
#[test]
#[cfg(feature = "test-git")]
fn transitive_dep_in_git_workspace_with_root() -> Result<()> {
let context = uv_test::test_context!("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "git-with-root"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"workspace-member-in-subdir",
]
[tool.uv.sources]
workspace-member-in-subdir = { git = "https://github.com/astral-sh/workspace-in-root-test", subdirectory = "workspace-member-in-subdir", rev = "d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68" }
"#
)?;
context.lock().assert().success();
let lock1: SourceLock =
toml::from_str(&fs_err::read_to_string(context.temp_dir.child("uv.lock"))?)?;
assert_json_snapshot!(lock1.sources(), @r#"
{
"git-with-root": {
"virtual": "."
},
"uv-git-workspace-in-root": {
"git": "https://github.com/astral-sh/workspace-in-root-test?rev=d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68#d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68"
},
"workspace-member-in-subdir": {
"git": "https://github.com/astral-sh/workspace-in-root-test?subdirectory=workspace-member-in-subdir&rev=d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68#d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68"
}
}
"#);
pyproject_toml.write_str(
r#"
[project]
name = "git-with-root"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"workspace-member-in-subdir",
"uv-git-workspace-in-root",
]
[tool.uv.sources]
workspace-member-in-subdir = { git = "https://github.com/astral-sh/workspace-in-root-test", subdirectory = "workspace-member-in-subdir", rev = "d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68" }
uv-git-workspace-in-root = { git = "https://github.com/astral-sh/workspace-in-root-test", rev = "d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68" }
"#
)?;
context.lock().assert().success();
let lock2: SourceLock =
toml::from_str(&fs_err::read_to_string(context.temp_dir.child("uv.lock"))?)?;
assert_eq!(lock1, lock2, "sources changed");
Ok(())
}
#[test]
fn workspace_members_with_leading_dot_slash() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[tool.uv.workspace]
members = ["./packages/foo", "./packages/bar"]
"#})?;
let deps = indoc! {r#"
dependencies = ["bar"]
[tool.uv.sources]
bar = { workspace = true }
"#};
make_project(&workspace.join("packages").join("foo"), "foo", deps)?;
let deps = indoc! {r"
dependencies = []
"};
make_project(&workspace.join("packages").join("bar"), "bar", deps)?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 2 packages in [TIME]
"
);
let lock: SourceLock = toml::from_str(&fs_err::read_to_string(workspace.join("uv.lock"))?)?;
assert_json_snapshot!(lock.sources(), @r#"
{
"bar": {
"editable": "packages/bar"
},
"foo": {
"editable": "packages/foo"
}
}
"#);
uv_snapshot!(context.filters(), context.sync().current_dir(workspace.join("packages").join("foo")), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: [TEMP_DIR]/workspace/.venv
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ bar==0.1.0 (from file://[TEMP_DIR]/workspace/packages/bar)
+ foo==0.1.0 (from file://[TEMP_DIR]/workspace/packages/foo)
"
);
Ok(())
}
#[test]
fn workspace_members_with_parent_directory() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[tool.uv.workspace]
members = ["../external-package"]
"#})?;
let deps = indoc! {r"
dependencies = []
"};
make_project(
&context.temp_dir.join("external-package"),
"external-package",
deps,
)?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 1 package in [TIME]
"
);
let lock: SourceLock = toml::from_str(&fs_err::read_to_string(workspace.join("uv.lock"))?)?;
assert_json_snapshot!(lock.sources(), @r#"
{
"external-package": {
"editable": "../external-package"
}
}
"#);
Ok(())
}
#[test]
fn workspace_members_with_complex_relative_paths() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[tool.uv.workspace]
members = ["./subdir/../../sibling-package"]
"#})?;
let deps = indoc! {r"
dependencies = []
"};
make_project(
&context.temp_dir.join("sibling-package"),
"sibling-package",
deps,
)?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 1 package in [TIME]
"
);
let lock: SourceLock = toml::from_str(&fs_err::read_to_string(workspace.join("uv.lock"))?)?;
assert_json_snapshot!(lock.sources(), @r#"
{
"sibling-package": {
"editable": "../sibling-package"
}
}
"#);
Ok(())
}
#[test]
fn workspace_unmanaged_member_no_project() -> Result<()> {
let context = uv_test::test_context!("3.12");
let workspace = context.temp_dir.child("workspace");
workspace.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "root"
version = "0.1.0"
requires-python = ">=3.12"
[tool.uv.workspace]
members = ["member"]
[build-system]
requires = ["uv_build>=0.9.0,<10000"]
build-backend = "uv_build"
"#})?;
fs_err::create_dir_all(workspace.join("src").join("root"))?;
fs_err::write(workspace.join("src").join("root").join("__init__.py"), "")?;
let member = workspace.join("member");
fs_err::create_dir_all(&member)?;
fs_err::write(
member.join("pyproject.toml"),
indoc! {"
[tool.uv]
managed = false
"},
)?;
context
.lock()
.arg("--verbose")
.current_dir(&workspace)
.assert()
.success();
Ok(())
}