mirror of https://github.com/astral-sh/uv
2094 lines
62 KiB
Rust
2094 lines
62 KiB
Rust
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 crate::common::{TestContext, 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 = "pypi")]
|
||
fn test_albatross_in_examples_bird_feeder() {
|
||
let context = TestContext::new("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), @r"
|
||
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), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Audited 2 packages in [TIME]
|
||
"
|
||
);
|
||
|
||
context.assert_file(current_dir.join("check_installed_bird_feeder.py"));
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn test_albatross_in_examples() {
|
||
let context = TestContext::new("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), @r"
|
||
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), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Audited 2 packages in [TIME]
|
||
"
|
||
);
|
||
|
||
context.assert_file(current_dir.join("check_installed_albatross.py"));
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn test_albatross_just_project() {
|
||
let context = TestContext::new("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), @r"
|
||
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), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Audited 2 packages in [TIME]
|
||
"
|
||
);
|
||
|
||
context.assert_file(current_dir.join("check_installed_albatross.py"));
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn test_albatross_project_in_excluded() {
|
||
let context = TestContext::new("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), @r"
|
||
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), @r"
|
||
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), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 2 packages in [TIME]
|
||
Audited 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), @r"
|
||
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 = "pypi")]
|
||
fn test_albatross_root_workspace() {
|
||
let context = TestContext::new("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), @r"
|
||
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), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 5 packages in [TIME]
|
||
Audited 5 packages in [TIME]
|
||
"
|
||
);
|
||
|
||
context.assert_file(current_dir.join("check_installed_albatross.py"));
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn test_albatross_root_workspace_bird_feeder() {
|
||
let context = TestContext::new("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), @r"
|
||
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), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 5 packages in [TIME]
|
||
Audited 4 packages in [TIME]
|
||
"
|
||
);
|
||
|
||
context.assert_file(current_dir.join("check_installed_bird_feeder.py"));
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn test_albatross_root_workspace_albatross() {
|
||
let context = TestContext::new("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), @r"
|
||
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), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 5 packages in [TIME]
|
||
Audited 4 packages in [TIME]
|
||
"
|
||
);
|
||
|
||
context.assert_file(current_dir.join("check_installed_albatross.py"));
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn test_albatross_virtual_workspace() {
|
||
let context = TestContext::new("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), @r"
|
||
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), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 7 packages in [TIME]
|
||
Audited 5 packages in [TIME]
|
||
"
|
||
);
|
||
|
||
context.assert_file(current_dir.join("check_installed_bird_feeder.py"));
|
||
}
|
||
|
||
/// Check that `uv run --package` works in a virtual workspace.
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn test_uv_run_with_package_virtual_workspace() -> Result<()> {
|
||
let context = TestContext::new("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]",
|
||
));
|
||
|
||
// Run from the `bird-feeder` member.
|
||
uv_snapshot!(filters, context
|
||
.run()
|
||
.arg("--package")
|
||
.arg("bird-feeder")
|
||
.arg("packages/bird-feeder/check_installed_bird_feeder.py")
|
||
.current_dir(&work_dir), @r"
|
||
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), @r"
|
||
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(())
|
||
}
|
||
|
||
/// Check that `uv run` works from a virtual workspace root, which should sync all packages in the
|
||
/// workspace.
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn test_uv_run_virtual_workspace_root() -> Result<()> {
|
||
let context = TestContext::new("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), @r"
|
||
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(())
|
||
}
|
||
|
||
/// Check that `uv run --package` works in a root workspace.
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn test_uv_run_with_package_root_workspace() -> Result<()> {
|
||
let context = TestContext::new("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), @r"
|
||
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), @r"
|
||
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(())
|
||
}
|
||
|
||
/// Check that `uv run --isolated` creates isolated virtual environments.
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn test_uv_run_isolate() -> Result<()> {
|
||
let context = TestContext::new("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]",
|
||
));
|
||
|
||
// Install the root package.
|
||
uv_snapshot!(context.filters(), context
|
||
.run()
|
||
.arg("--package")
|
||
.arg("albatross")
|
||
.arg("check_installed_albatross.py")
|
||
.current_dir(&work_dir), @r"
|
||
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)
|
||
"
|
||
);
|
||
|
||
// Run in `bird-feeder`. We shouldn't be able to import `albatross`, but we _can_ due to our
|
||
// virtual environment semantics. Specifically, we only make the changes necessary to run a
|
||
// given command, so we don't remove `albatross` from the environment.
|
||
uv_snapshot!(filters, context
|
||
.run()
|
||
.arg("--package")
|
||
.arg("bird-feeder")
|
||
.arg("check_installed_albatross.py")
|
||
.current_dir(&work_dir), @r"
|
||
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]
|
||
Audited 4 packages in [TIME]
|
||
"
|
||
);
|
||
|
||
// If we `--isolated`, though, we use an isolated virtual environment, so `albatross` is not
|
||
// available.
|
||
// TODO(charlie): This should show the resolution output, but `--isolated` is coupled to
|
||
// `--no-project` right now.
|
||
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(())
|
||
}
|
||
|
||
/// Check that the resolution is the same no matter where in the workspace we are.
|
||
fn workspace_lock_idempotence(workspace: &str, subdirectories: &[&str]) -> Result<()> {
|
||
let mut shared_lock = None;
|
||
|
||
for dir in subdirectories {
|
||
let context = TestContext::new("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"))?;
|
||
// Check the lockfile is the same for all resolutions.
|
||
if let Some(shared_lock) = &shared_lock {
|
||
assert_eq!(shared_lock, &lock);
|
||
} else {
|
||
shared_lock = Some(lock);
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Check that the resolution is the same no matter where in the workspace we are.
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn workspace_lock_idempotence_root_workspace() -> Result<()> {
|
||
workspace_lock_idempotence(
|
||
"albatross-root-workspace",
|
||
&[".", "packages/bird-feeder", "packages/seeds"],
|
||
)?;
|
||
Ok(())
|
||
}
|
||
|
||
/// Check that the resolution is the same no matter where in the workspace we are, and that locking
|
||
/// works even if there is no root project.
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn workspace_lock_idempotence_virtual_workspace() -> Result<()> {
|
||
workspace_lock_idempotence(
|
||
"albatross-virtual-workspace",
|
||
&[
|
||
".",
|
||
"packages/albatross",
|
||
"packages/bird-feeder",
|
||
"packages/seeds",
|
||
],
|
||
)?;
|
||
Ok(())
|
||
}
|
||
|
||
/// Extract just the sources from the lockfile, to test path resolution.
|
||
#[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 path dependencies from one workspace into another.
|
||
///
|
||
/// We have a main workspace with packages `a` and `b`, and a second workspace with `c`, `d` and
|
||
/// `e`. We have `a -> b`, `b -> c`, `c -> d`. `e` should not be installed.
|
||
#[test]
|
||
fn workspace_to_workspace_paths_dependencies() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Build the main workspace ...
|
||
let main_workspace = context.temp_dir.child("main-workspace");
|
||
main_workspace
|
||
.child("pyproject.toml")
|
||
.write_str(indoc! {r#"
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#})?;
|
||
|
||
// ... with a ...
|
||
let deps = indoc! {r#"
|
||
dependencies = ["b"]
|
||
|
||
[tool.uv.sources]
|
||
b = { workspace = true }
|
||
"#};
|
||
make_project(&main_workspace.join("packages").join("a"), "a", deps)?;
|
||
|
||
// ... and b.
|
||
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)?;
|
||
|
||
// Build the second workspace ...
|
||
let other_workspace = context.temp_dir.child("other-workspace");
|
||
other_workspace
|
||
.child("pyproject.toml")
|
||
.write_str(indoc! {r#"
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#})?;
|
||
|
||
// ... with c ...
|
||
let deps = indoc! {r#"
|
||
dependencies = ["d"]
|
||
|
||
[tool.uv.sources]
|
||
d = { workspace = true }
|
||
"#};
|
||
make_project(&other_workspace.join("packages").join("c"), "c", deps)?;
|
||
|
||
// ... and d ...
|
||
let deps = indoc! {r"
|
||
dependencies = []
|
||
"};
|
||
make_project(&other_workspace.join("packages").join("d"), "d", deps)?;
|
||
|
||
// ... and e.
|
||
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), @r###"
|
||
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(())
|
||
}
|
||
|
||
/// Ensure that workspace discovery errors if a member is missing a `pyproject.toml`.
|
||
#[test]
|
||
fn workspace_empty_member() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Build the main workspace ...
|
||
let workspace = context.temp_dir.child("workspace");
|
||
workspace.child("pyproject.toml").write_str(indoc! {r#"
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#})?;
|
||
|
||
// ... with a ...
|
||
let deps = indoc! {r#"
|
||
dependencies = ["b"]
|
||
|
||
[tool.uv.sources]
|
||
b = { workspace = true }
|
||
"#};
|
||
make_project(&workspace.join("packages").join("a"), "a", deps)?;
|
||
|
||
// ... and b.
|
||
let deps = indoc! {r"
|
||
"};
|
||
make_project(&workspace.join("packages").join("b"), "b", deps)?;
|
||
|
||
// ... and an empty c.
|
||
fs_err::create_dir_all(workspace.join("packages").join("c"))?;
|
||
|
||
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r###"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Workspace member `[TEMP_DIR]/workspace/packages/c` is missing a `pyproject.toml` (matches: `packages/*`)
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Ensure that workspace discovery ignores hidden directories.
|
||
#[test]
|
||
fn workspace_hidden_files() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Build the main workspace ...
|
||
let workspace = context.temp_dir.child("workspace");
|
||
workspace.child("pyproject.toml").write_str(indoc! {r#"
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#})?;
|
||
|
||
// ... with a ...
|
||
let deps = indoc! {r#"
|
||
dependencies = ["b"]
|
||
|
||
[tool.uv.sources]
|
||
b = { workspace = true }
|
||
"#};
|
||
make_project(&workspace.join("packages").join("a"), "a", deps)?;
|
||
|
||
// ... and b.
|
||
let deps = indoc! {r"
|
||
"};
|
||
make_project(&workspace.join("packages").join("b"), "b", deps)?;
|
||
|
||
// ... and a hidden c.
|
||
fs_err::create_dir_all(workspace.join("packages").join(".c"))?;
|
||
|
||
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r###"
|
||
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(())
|
||
}
|
||
|
||
/// Ensure that workspace discovery accepts valid hidden directories.
|
||
#[test]
|
||
fn workspace_hidden_member() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Build the main workspace ...
|
||
let workspace = context.temp_dir.child("workspace");
|
||
workspace.child("pyproject.toml").write_str(indoc! {r#"
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#})?;
|
||
|
||
// ... with a ...
|
||
let deps = indoc! {r#"
|
||
dependencies = ["b"]
|
||
|
||
[tool.uv.sources]
|
||
b = { workspace = true }
|
||
"#};
|
||
make_project(&workspace.join("packages").join("a"), "a", deps)?;
|
||
|
||
// ... and b.
|
||
let deps = indoc! {r#"
|
||
dependencies = ["c"]
|
||
|
||
[tool.uv.sources]
|
||
c = { workspace = true }
|
||
"#};
|
||
make_project(&workspace.join("packages").join("b"), "b", deps)?;
|
||
|
||
// ... and a hidden (but valid) .c.
|
||
let deps = indoc! {r"
|
||
dependencies = []
|
||
"};
|
||
make_project(&workspace.join("packages").join(".c"), "c", deps)?;
|
||
|
||
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r###"
|
||
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(())
|
||
}
|
||
|
||
/// Ensure that workspace discovery accepts valid hidden directories.
|
||
#[test]
|
||
fn workspace_non_included_member() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Build the main workspace ...
|
||
let workspace = context.temp_dir.child("workspace");
|
||
workspace.child("pyproject.toml").write_str(indoc! {r#"
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#})?;
|
||
|
||
// ... with a ...
|
||
let deps = indoc! {r#"
|
||
dependencies = ["b"]
|
||
|
||
[tool.uv.sources]
|
||
b = { workspace = true }
|
||
"#};
|
||
make_project(&workspace.join("packages").join("a"), "a", deps)?;
|
||
|
||
// ... and b.
|
||
let deps = indoc! {r"
|
||
dependencies = []
|
||
"};
|
||
make_project(&workspace.join("packages").join("b"), "b", deps)?;
|
||
|
||
// ... and c, which is _not_ a member, but also isn't explicitly excluded.
|
||
let deps = indoc! {r"
|
||
dependencies = []
|
||
"};
|
||
make_project(&workspace.join("c"), "c", deps)?;
|
||
|
||
// Locking from `c` should not include any workspace members.
|
||
uv_snapshot!(context.filters(), context.lock().current_dir(workspace.join("c")), @r###"
|
||
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(())
|
||
}
|
||
|
||
/// Ensure workspace members inherit sources from the root, if not specified in the member.
|
||
///
|
||
/// In such cases, relative paths should be resolved relative to the workspace root, rather than
|
||
/// relative to the member.
|
||
#[test]
|
||
fn workspace_inherit_sources() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Create the workspace root.
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#})?;
|
||
workspace.child("src/__init__.py").touch()?;
|
||
|
||
// Create a package.
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#})?;
|
||
leaf.child("src/__init__.py").touch()?;
|
||
|
||
// Create a peripheral library.
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#})?;
|
||
library.child("src/__init__.py").touch()?;
|
||
|
||
// As-is, resolving should fail.
|
||
uv_snapshot!(context.filters(), context.lock().arg("--offline").current_dir(&workspace), @r###"
|
||
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.
|
||
"###
|
||
);
|
||
|
||
// Update the leaf to include the source.
|
||
leaf.child("pyproject.toml").write_str(indoc! {r#"
|
||
[project]
|
||
name = "leaf"
|
||
version = "0.1.0"
|
||
dependencies = ["library"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.sources]
|
||
library = { path = "../../../library", editable = true }
|
||
"#})?;
|
||
leaf.child("src/__init__.py").touch()?;
|
||
|
||
// Resolving should succeed.
|
||
uv_snapshot!(context.filters(), context.lock().arg("--offline").current_dir(&workspace), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||
Resolved 3 packages in [TIME]
|
||
"###
|
||
);
|
||
|
||
// Revert that change.
|
||
leaf.child("pyproject.toml").write_str(indoc! {r#"
|
||
[project]
|
||
name = "leaf"
|
||
version = "0.1.0"
|
||
dependencies = ["library"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#})?;
|
||
|
||
// Update the root to include the source.
|
||
workspace.child("pyproject.toml").write_str(indoc! {r#"
|
||
[project]
|
||
name = "workspace"
|
||
version = "0.1.0"
|
||
dependencies = []
|
||
requires-python = ">=3.12"
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.sources]
|
||
library = { path = "../library", editable = true }
|
||
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#})?;
|
||
|
||
// Resolving should succeed.
|
||
uv_snapshot!(context.filters(), context.lock().arg("--offline").current_dir(&workspace), @r###"
|
||
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();
|
||
|
||
// The lockfile should use a path relative to the workspace root.
|
||
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 = "." }
|
||
"#
|
||
);
|
||
});
|
||
|
||
// Update the root to include the source again.
|
||
workspace.child("pyproject.toml").write_str(indoc! {r#"
|
||
[project]
|
||
name = "workspace"
|
||
version = "0.1.0"
|
||
dependencies = []
|
||
requires-python = ">=3.12"
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.sources]
|
||
library = { path = "../library", editable = true }
|
||
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#})?;
|
||
|
||
// Update the member to include a _different_ source.
|
||
leaf.child("pyproject.toml").write_str(indoc! {r#"
|
||
[project]
|
||
name = "leaf"
|
||
version = "0.1.0"
|
||
dependencies = ["library"]
|
||
|
||
[build-system]
|
||
requires = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.sources]
|
||
application = { path = "../application", editable = true }
|
||
"#})?;
|
||
|
||
// Resolving should succeed; the member should still use the root's source, despite defining
|
||
// some of its own
|
||
uv_snapshot!(context.filters(), context.lock().arg("--offline").current_dir(&workspace), @r###"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||
Resolved 3 packages in [TIME]
|
||
"###
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Tests error messages when a workspace member's dependencies cannot be resolved.
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn workspace_unsatisfiable_member_dependencies() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Create the workspace root.
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#})?;
|
||
workspace.child("src/__init__.py").touch()?;
|
||
|
||
// Create a package that requires a dependency that does not exist.
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#})?;
|
||
leaf.child("src/__init__.py").touch()?;
|
||
|
||
// Resolving should fail.
|
||
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r"
|
||
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(())
|
||
}
|
||
|
||
/// Tests error messages when a workspace member's dependencies conflict with
|
||
/// another member's.
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn workspace_unsatisfiable_member_dependencies_conflicting() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Create the workspace root.
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#})?;
|
||
workspace.child("src/__init__.py").touch()?;
|
||
|
||
// Create two workspace members with incompatible pins
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#})?;
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#})?;
|
||
bar.child("src/__init__.py").touch()?;
|
||
|
||
// Resolving should fail.
|
||
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r###"
|
||
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(())
|
||
}
|
||
|
||
/// Tests error messages when a workspace member's dependencies conflict with
|
||
/// two other member's.
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn workspace_unsatisfiable_member_dependencies_conflicting_threeway() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Create the workspace root.
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#})?;
|
||
workspace.child("src/__init__.py").touch()?;
|
||
|
||
// Create three workspace members with incompatible pins.
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#})?;
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#})?;
|
||
knot.child("src/__init__.py").touch()?;
|
||
|
||
// We'll raise the first conflict in the resolver, so `bird` shouldn't be
|
||
// present in the error even though it also incompatible
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#})?;
|
||
bird.child("src/__init__.py").touch()?;
|
||
|
||
// Resolving should fail.
|
||
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r###"
|
||
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(())
|
||
}
|
||
|
||
/// Tests error messages when a workspace member's dependencies conflict with
|
||
/// another member's optional dependencies.
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn workspace_unsatisfiable_member_dependencies_conflicting_extra() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Create the workspace root.
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#})?;
|
||
workspace.child("src/__init__.py").touch()?;
|
||
|
||
// Create two workspace members with incompatible pins
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#})?;
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#})?;
|
||
bar.child("src/__init__.py").touch()?;
|
||
|
||
// Resolving should fail.
|
||
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r###"
|
||
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(())
|
||
}
|
||
|
||
/// Tests error messages when a workspace member's dependencies conflict with
|
||
/// another member's development dependencies.
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn workspace_unsatisfiable_member_dependencies_conflicting_dev() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Create the workspace root.
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#})?;
|
||
workspace.child("src/__init__.py").touch()?;
|
||
|
||
// Create two workspace members with incompatible pins
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#})?;
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv]
|
||
dev-dependencies = ["anyio==4.2.0"]
|
||
"#})?;
|
||
bar.child("src/__init__.py").touch()?;
|
||
|
||
// Resolving should fail.
|
||
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r"
|
||
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(())
|
||
}
|
||
|
||
/// Tests error messages when a workspace member's name shadows a dependency of
|
||
/// another member.
|
||
#[test]
|
||
#[cfg(feature = "pypi")]
|
||
fn workspace_member_name_shadows_dependencies() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Create the workspace root.
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
|
||
[tool.uv.workspace]
|
||
members = ["packages/*"]
|
||
"#})?;
|
||
workspace.child("src/__init__.py").touch()?;
|
||
|
||
// Create a workspace member that depends on `anyio`
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#})?;
|
||
foo.child("src/__init__.py").touch()?;
|
||
|
||
// Then create an `anyio` workspace member
|
||
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 = ["setuptools>=42"]
|
||
build-backend = "setuptools.build_meta"
|
||
"#})?;
|
||
anyio.child("src/__init__.py").touch()?;
|
||
|
||
// We should fail
|
||
// TODO(zanieb): This error message is bad?
|
||
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r###"
|
||
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 that path dependencies with path dependencies resolve paths correctly across workspaces.
|
||
///
|
||
/// Each package is its own workspace. We put the other projects into a separate directory `libs` so
|
||
/// the paths don't line up by accident.
|
||
#[test]
|
||
fn test_path_hopping() -> Result<()> {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Build the main project ...
|
||
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)?;
|
||
|
||
// ... that depends on foo ...
|
||
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)?;
|
||
|
||
// ... that depends on bar, a stub project.
|
||
make_project(&context.temp_dir.join("libs").join("bar"), "bar", "")?;
|
||
|
||
uv_snapshot!(context.filters(), context.lock().arg("--preview").current_dir(&main_project_dir), @r###"
|
||
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(())
|
||
}
|
||
|
||
/// `c` is a package in a git workspace, and it has a workspace dependency to `d`. Check that we
|
||
/// are correctly resolving `d` to a git dependency with a subdirectory and not relative to the
|
||
/// checkout directory.
|
||
#[test]
|
||
#[cfg(feature = "git")]
|
||
fn transitive_dep_in_git_workspace_no_root() -> Result<()> {
|
||
let context = TestContext::new("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"
|
||
}
|
||
}
|
||
"###);
|
||
|
||
// Check that we don't report a conflict here either.
|
||
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(())
|
||
}
|
||
|
||
/// `workspace-member-in-subdir` is a package in a git workspace, and it has a workspace dependency
|
||
/// to `uv-git-workspace-in-root`. Check that we are correctly resolving `uv-git-workspace-in-root`
|
||
/// to a git dependency without a subdirectory and not relative to the checkout directory.
|
||
#[test]
|
||
#[cfg(feature = "git")]
|
||
fn transitive_dep_in_git_workspace_with_root() -> Result<()> {
|
||
let context = TestContext::new("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"
|
||
}
|
||
}
|
||
"###);
|
||
|
||
// Check that we don't report a conflict here either
|
||
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 = TestContext::new("3.12");
|
||
|
||
// Build the main workspace with leading `./` in member paths
|
||
let workspace = context.temp_dir.child("workspace");
|
||
workspace.child("pyproject.toml").write_str(indoc! {r#"
|
||
[tool.uv.workspace]
|
||
members = ["./packages/foo", "./packages/bar"]
|
||
"#})?;
|
||
|
||
// Create package foo that depends on bar
|
||
let deps = indoc! {r#"
|
||
dependencies = ["bar"]
|
||
|
||
[tool.uv.sources]
|
||
bar = { workspace = true }
|
||
"#};
|
||
make_project(&workspace.join("packages").join("foo"), "foo", deps)?;
|
||
|
||
// Create package bar
|
||
let deps = indoc! {r"
|
||
dependencies = []
|
||
"};
|
||
make_project(&workspace.join("packages").join("bar"), "bar", deps)?;
|
||
|
||
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r###"
|
||
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"
|
||
}
|
||
}
|
||
"###);
|
||
|
||
// Test syncing from within foo works correctly
|
||
uv_snapshot!(context.filters(), context.sync().current_dir(workspace.join("packages").join("foo")), @r###"
|
||
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 = TestContext::new("3.12");
|
||
|
||
// Build a workspace with a member outside its directory using `../`
|
||
let workspace = context.temp_dir.child("workspace");
|
||
workspace.child("pyproject.toml").write_str(indoc! {r#"
|
||
[tool.uv.workspace]
|
||
members = ["../external-package"]
|
||
"#})?;
|
||
|
||
// Create an 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), @r###"
|
||
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 = TestContext::new("3.12");
|
||
|
||
// Build a workspace with complex relative path normalization
|
||
let workspace = context.temp_dir.child("workspace");
|
||
workspace.child("pyproject.toml").write_str(indoc! {r#"
|
||
[tool.uv.workspace]
|
||
members = ["./subdir/../../sibling-package"]
|
||
"#})?;
|
||
|
||
// Create a 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), @r###"
|
||
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(())
|
||
}
|