mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
Add uv run --package
(#3864)
Add a `--package` option that allows switching the current project in the workspace. Wherever you are in a workspace, you should be able to run with any other project as root. This is the uv equivalent of `cargo run -p`. I don't love the `--package` name, esp. since `-p` is already taken and in general to many things start with p already. Part of this change is moving the workspace discovery of `ProjectWorkspace` to `Workspace` itself. ## Usage In albatross-virtual-workspace: ```console $ uv venv $ uv run --preview --package bird-feeder python -c "import albatross" Built file:///home/konsti/projects/uv/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder Built file:///home/konsti/projects/uv/scripts/workspaces/albatross-virtual-workspace/packages/seeds Built 2 editables in 167ms Resolved 5 packages in 4ms Installed 5 packages in 1ms + anyio==4.4.0 + bird-feeder==1.0.0 (from file:///home/konsti/projects/uv/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder) + idna==3.6 + seeds==1.0.0 (from file:///home/konsti/projects/uv/scripts/workspaces/albatross-virtual-workspace/packages/seeds) + sniffio==1.3.1 Traceback (most recent call last): File "<string>", line 1, in <module> ModuleNotFoundError: No module named 'albatross' $ uv venv $ uv run --preview --package albatross python -c "import albatross" Built file:///home/konsti/projects/uv/scripts/workspaces/albatross-virtual-workspace/packages/albatross Built file:///home/konsti/projects/uv/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder Built file:///home/konsti/projects/uv/scripts/workspaces/albatross-virtual-workspace/packages/seeds Built 3 editables in 173ms Resolved 7 packages in 6ms Installed 7 packages in 1ms + albatross==0.1.0 (from file:///home/konsti/projects/uv/scripts/workspaces/albatross-virtual-workspace/packages/albatross) + anyio==4.4.0 + bird-feeder==1.0.0 (from file:///home/konsti/projects/uv/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder) + idna==3.6 + seeds==1.0.0 (from file:///home/konsti/projects/uv/scripts/workspaces/albatross-virtual-workspace/packages/seeds) + sniffio==1.3.1 + tqdm==4.66.4 ``` In albatross-root-workspace: ```console $ uv venv $ uv run --preview --package bird-feeder python -c "import albatross" Using Python 3.12.3 interpreter at: /home/konsti/.local/bin/python3 Creating virtualenv at: .venv Activate with: source .venv/bin/activate Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s Running `/home/konsti/projects/uv/target/debug/uv run --preview --package bird-feeder python -c 'import albatross'` Built file:///home/konsti/projects/uv/scripts/workspaces/albatross-root-workspace/packages/bird-feeder Built file:///home/konsti/projects/uv/scripts/workspaces/albatross-root-workspace/packages/seeds Built 2 editables in 161ms Resolved 5 packages in 4ms Installed 5 packages in 1ms + anyio==4.4.0 + bird-feeder==1.0.0 (from file:///home/konsti/projects/uv/scripts/workspaces/albatross-root-workspace/packages/bird-feeder) + idna==3.6 + seeds==1.0.0 (from file:///home/konsti/projects/uv/scripts/workspaces/albatross-root-workspace/packages/seeds) + sniffio==1.3.1 Traceback (most recent call last): File "<string>", line 1, in <module> ModuleNotFoundError: No module named 'albatross' $ uv venv $ cargo run run --preview --package albatross python -c "import albatross" Using Python 3.12.3 interpreter at: /home/konsti/.local/bin/python3 Creating virtualenv at: .venv Activate with: source .venv/bin/activate Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s Running `/home/konsti/projects/uv/target/debug/uv run --preview --package albatross python -c 'import albatross'` Built file:///home/konsti/projects/uv/scripts/workspaces/albatross-root-workspace Built file:///home/konsti/projects/uv/scripts/workspaces/albatross-root-workspace/packages/bird-feeder Built file:///home/konsti/projects/uv/scripts/workspaces/albatross-root-workspace/packages/seeds Built 3 editables in 168ms Resolved 7 packages in 5ms Installed 7 packages in 1ms + albatross==0.1.0 (from file:///home/konsti/projects/uv/scripts/workspaces/albatross-root-workspace) + anyio==4.4.0 + bird-feeder==1.0.0 (from file:///home/konsti/projects/uv/scripts/workspaces/albatross-root-workspace/packages/bird-feeder) + idna==3.6 + seeds==1.0.0 (from file:///home/konsti/projects/uv/scripts/workspaces/albatross-root-workspace/packages/seeds) + sniffio==1.3.1 + tqdm==4.66.4 ```
This commit is contained in:
parent
0d0308c531
commit
01d1a39c21
13 changed files with 472 additions and 110 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4775,6 +4775,7 @@ dependencies = [
|
||||||
"pypi-types",
|
"pypi-types",
|
||||||
"rayon",
|
"rayon",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
|
"same-file",
|
||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//! Resolve the current [`ProjectWorkspace`].
|
//! Resolve the current [`ProjectWorkspace`] or [`Workspace`].
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
@ -17,18 +17,24 @@ use crate::pyproject::{PyProjectToml, Source, ToolUvWorkspace};
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum WorkspaceError {
|
pub enum WorkspaceError {
|
||||||
|
// Workspace structure errors.
|
||||||
#[error("No `pyproject.toml` found in current directory or any parent directory")]
|
#[error("No `pyproject.toml` found in current directory or any parent directory")]
|
||||||
MissingPyprojectToml,
|
MissingPyprojectToml,
|
||||||
|
#[error("No `project` table found in: `{}`", _0.simplified_display())]
|
||||||
|
MissingProject(PathBuf),
|
||||||
|
#[error("No workspace found for: `{}`", _0.simplified_display())]
|
||||||
|
MissingWorkspace(PathBuf),
|
||||||
|
#[error("pyproject.toml section is declared as dynamic, but must be static: `{0}`")]
|
||||||
|
DynamicNotAllowed(&'static str),
|
||||||
#[error("Failed to find directories for glob: `{0}`")]
|
#[error("Failed to find directories for glob: `{0}`")]
|
||||||
Pattern(String, #[source] PatternError),
|
Pattern(String, #[source] PatternError),
|
||||||
|
// Syntax and other errors.
|
||||||
#[error("Invalid glob in `tool.uv.workspace.members`: `{0}`")]
|
#[error("Invalid glob in `tool.uv.workspace.members`: `{0}`")]
|
||||||
Glob(String, #[source] GlobError),
|
Glob(String, #[source] GlobError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
#[error("Failed to parse: `{}`", _0.user_display())]
|
#[error("Failed to parse: `{}`", _0.user_display())]
|
||||||
Toml(PathBuf, #[source] Box<toml::de::Error>),
|
Toml(PathBuf, #[source] Box<toml::de::Error>),
|
||||||
#[error("No `project` table found in: `{}`", _0.simplified_display())]
|
|
||||||
MissingProject(PathBuf),
|
|
||||||
#[error("Failed to normalize workspace member path")]
|
#[error("Failed to normalize workspace member path")]
|
||||||
Normalize(#[source] std::io::Error),
|
Normalize(#[source] std::io::Error),
|
||||||
}
|
}
|
||||||
|
@ -48,6 +54,105 @@ pub struct Workspace {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Workspace {
|
impl Workspace {
|
||||||
|
/// Find the workspace containing the given path.
|
||||||
|
///
|
||||||
|
/// Unlike the [`ProjectWorkspace`] discovery, this does not require a current project.
|
||||||
|
///
|
||||||
|
/// Steps of workspace discovery: Start by looking at the closest `pyproject.toml`:
|
||||||
|
/// * If it's an explicit workspace root: Collect workspace from this root, we're done.
|
||||||
|
/// * If it's also not a project: Error, must be either a workspace root or a project.
|
||||||
|
/// * Otherwise, try to find an explicit workspace root above:
|
||||||
|
/// * If an explicit workspace root exists: Collect workspace from this root, we're done.
|
||||||
|
/// * If there is no explicit workspace: We have a single project workspace, we're done.
|
||||||
|
pub async fn discover(
|
||||||
|
path: &Path,
|
||||||
|
stop_discovery_at: Option<&Path>,
|
||||||
|
) -> Result<Workspace, WorkspaceError> {
|
||||||
|
let project_root = path
|
||||||
|
.ancestors()
|
||||||
|
.find(|path| path.join("pyproject.toml").is_file())
|
||||||
|
.ok_or(WorkspaceError::MissingPyprojectToml)?;
|
||||||
|
|
||||||
|
let pyproject_path = project_root.join("pyproject.toml");
|
||||||
|
let contents = fs_err::tokio::read_to_string(&pyproject_path).await?;
|
||||||
|
let pyproject_toml: PyProjectToml = toml::from_str(&contents)
|
||||||
|
.map_err(|err| WorkspaceError::Toml(pyproject_path.clone(), Box::new(err)))?;
|
||||||
|
|
||||||
|
let project_path = absolutize_path(project_root)
|
||||||
|
.map_err(WorkspaceError::Normalize)?
|
||||||
|
.to_path_buf();
|
||||||
|
|
||||||
|
// Check if the current project is also an explicit workspace root.
|
||||||
|
let explicit_root = pyproject_toml
|
||||||
|
.tool
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|tool| tool.uv.as_ref())
|
||||||
|
.and_then(|uv| uv.workspace.as_ref())
|
||||||
|
.map(|workspace| {
|
||||||
|
(
|
||||||
|
project_path.clone(),
|
||||||
|
workspace.clone(),
|
||||||
|
pyproject_toml.clone(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let (workspace_root, workspace_definition, workspace_pyproject_toml) =
|
||||||
|
if let Some(workspace) = explicit_root {
|
||||||
|
workspace
|
||||||
|
} else if pyproject_toml.project.is_none() {
|
||||||
|
return Err(WorkspaceError::MissingProject(project_path));
|
||||||
|
} else if let Some(workspace) = find_workspace(&project_path, stop_discovery_at).await?
|
||||||
|
{
|
||||||
|
workspace
|
||||||
|
} else {
|
||||||
|
return Err(WorkspaceError::MissingWorkspace(project_path));
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Found workspace root: `{}`",
|
||||||
|
workspace_root.simplified_display()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Unlike in `ProjectWorkspace` discovery, we might be in a virtual workspace root without
|
||||||
|
// being in any specific project.
|
||||||
|
let current_project = pyproject_toml
|
||||||
|
.project
|
||||||
|
.clone()
|
||||||
|
.map(|project| (project.name.clone(), project_path, pyproject_toml));
|
||||||
|
Self::collect_members(
|
||||||
|
workspace_root,
|
||||||
|
workspace_definition,
|
||||||
|
workspace_pyproject_toml,
|
||||||
|
current_project,
|
||||||
|
stop_discovery_at,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the current project to the given workspace member.
|
||||||
|
///
|
||||||
|
/// Returns `None` if the package is not part of the workspace.
|
||||||
|
pub fn with_current_project(self, package_name: PackageName) -> Option<ProjectWorkspace> {
|
||||||
|
let member = self.packages.get(&package_name)?;
|
||||||
|
let extras = member
|
||||||
|
.pyproject_toml
|
||||||
|
.project
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|project| project.optional_dependencies.as_ref())
|
||||||
|
.map(|optional_dependencies| {
|
||||||
|
let mut extras = optional_dependencies.keys().cloned().collect::<Vec<_>>();
|
||||||
|
extras.sort_unstable();
|
||||||
|
extras
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
Some(ProjectWorkspace {
|
||||||
|
project_root: member.root().clone(),
|
||||||
|
project_name: package_name,
|
||||||
|
extras,
|
||||||
|
workspace: self,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// The path to the workspace root, the directory containing the top level `pyproject.toml` with
|
/// The path to the workspace root, the directory containing the top level `pyproject.toml` with
|
||||||
/// the `uv.tool.workspace`, or the `pyproject.toml` in an implicit single workspace project.
|
/// the `uv.tool.workspace`, or the `pyproject.toml` in an implicit single workspace project.
|
||||||
pub fn root(&self) -> &PathBuf {
|
pub fn root(&self) -> &PathBuf {
|
||||||
|
@ -63,6 +168,123 @@ impl Workspace {
|
||||||
pub fn sources(&self) -> &BTreeMap<PackageName, Source> {
|
pub fn sources(&self) -> &BTreeMap<PackageName, Source> {
|
||||||
&self.sources
|
&self.sources
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collect the workspace member projects from the `members` and `excludes` entries.
|
||||||
|
async fn collect_members(
|
||||||
|
workspace_root: PathBuf,
|
||||||
|
workspace_definition: ToolUvWorkspace,
|
||||||
|
workspace_pyproject_toml: PyProjectToml,
|
||||||
|
current_project: Option<(PackageName, PathBuf, PyProjectToml)>,
|
||||||
|
stop_discovery_at: Option<&Path>,
|
||||||
|
) -> Result<Workspace, WorkspaceError> {
|
||||||
|
let mut workspace_members = BTreeMap::new();
|
||||||
|
// Avoid reading a `pyproject.toml` more than once.
|
||||||
|
let mut seen = FxHashSet::default();
|
||||||
|
|
||||||
|
// Add the project at the workspace root, if it exists and if it's distinct from the current
|
||||||
|
// project.
|
||||||
|
if current_project
|
||||||
|
.as_ref()
|
||||||
|
.map(|(_, path, _)| path != &workspace_root)
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
if let Some(project) = &workspace_pyproject_toml.project {
|
||||||
|
let pyproject_path = workspace_root.join("pyproject.toml");
|
||||||
|
let contents = fs_err::read_to_string(&pyproject_path)?;
|
||||||
|
let pyproject_toml = toml::from_str(&contents)
|
||||||
|
.map_err(|err| WorkspaceError::Toml(pyproject_path, Box::new(err)))?;
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Adding root workspace member: {}",
|
||||||
|
workspace_root.simplified_display()
|
||||||
|
);
|
||||||
|
|
||||||
|
seen.insert(workspace_root.clone());
|
||||||
|
workspace_members.insert(
|
||||||
|
project.name.clone(),
|
||||||
|
WorkspaceMember {
|
||||||
|
root: workspace_root.clone(),
|
||||||
|
pyproject_toml,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The current project is a workspace member, especially in a single project workspace.
|
||||||
|
if let Some((project_name, project_path, project)) = current_project {
|
||||||
|
debug!(
|
||||||
|
"Adding current workspace member: {}",
|
||||||
|
project_path.simplified_display()
|
||||||
|
);
|
||||||
|
|
||||||
|
seen.insert(project_path.clone());
|
||||||
|
workspace_members.insert(
|
||||||
|
project_name,
|
||||||
|
WorkspaceMember {
|
||||||
|
root: project_path.clone(),
|
||||||
|
pyproject_toml: project.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all other workspace members.
|
||||||
|
for member_glob in workspace_definition.members.unwrap_or_default() {
|
||||||
|
let absolute_glob = workspace_root
|
||||||
|
.simplified()
|
||||||
|
.join(member_glob.as_str())
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
for member_root in glob(&absolute_glob)
|
||||||
|
.map_err(|err| WorkspaceError::Pattern(absolute_glob.to_string(), err))?
|
||||||
|
{
|
||||||
|
let member_root = member_root
|
||||||
|
.map_err(|err| WorkspaceError::Glob(absolute_glob.to_string(), err))?;
|
||||||
|
if !seen.insert(member_root.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let member_root = absolutize_path(&member_root)
|
||||||
|
.map_err(WorkspaceError::Normalize)?
|
||||||
|
.to_path_buf();
|
||||||
|
|
||||||
|
trace!("Processing workspace member {}", member_root.user_display());
|
||||||
|
|
||||||
|
// Read the member `pyproject.toml`.
|
||||||
|
let pyproject_path = member_root.join("pyproject.toml");
|
||||||
|
let contents = fs_err::tokio::read_to_string(&pyproject_path).await?;
|
||||||
|
let pyproject_toml: PyProjectToml = toml::from_str(&contents)
|
||||||
|
.map_err(|err| WorkspaceError::Toml(pyproject_path, Box::new(err)))?;
|
||||||
|
|
||||||
|
// Extract the package name.
|
||||||
|
let Some(project) = pyproject_toml.project.clone() else {
|
||||||
|
return Err(WorkspaceError::MissingProject(member_root));
|
||||||
|
};
|
||||||
|
|
||||||
|
let member = WorkspaceMember {
|
||||||
|
root: member_root.clone(),
|
||||||
|
pyproject_toml,
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Adding discovered workspace member: {}",
|
||||||
|
member_root.simplified_display()
|
||||||
|
);
|
||||||
|
workspace_members.insert(project.name, member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let workspace_sources = workspace_pyproject_toml
|
||||||
|
.tool
|
||||||
|
.and_then(|tool| tool.uv)
|
||||||
|
.and_then(|uv| uv.sources)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
check_nested_workspaces(&workspace_root, stop_discovery_at);
|
||||||
|
|
||||||
|
Ok(Workspace {
|
||||||
|
root: workspace_root,
|
||||||
|
packages: workspace_members,
|
||||||
|
sources: workspace_sources,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A project in a workspace.
|
/// A project in a workspace.
|
||||||
|
@ -183,11 +405,10 @@ impl ProjectWorkspace {
|
||||||
/// `stop_discovery_at` must be either `None` or an ancestor of the current directory. If set,
|
/// `stop_discovery_at` must be either `None` or an ancestor of the current directory. If set,
|
||||||
/// only directories between the current path and `stop_discovery_at` are considered.
|
/// only directories between the current path and `stop_discovery_at` are considered.
|
||||||
pub async fn discover(
|
pub async fn discover(
|
||||||
path: impl AsRef<Path>,
|
path: &Path,
|
||||||
stop_discovery_at: Option<&Path>,
|
stop_discovery_at: Option<&Path>,
|
||||||
) -> Result<Self, WorkspaceError> {
|
) -> Result<Self, WorkspaceError> {
|
||||||
let project_root = path
|
let project_root = path
|
||||||
.as_ref()
|
|
||||||
.ancestors()
|
.ancestors()
|
||||||
.take_while(|path| {
|
.take_while(|path| {
|
||||||
// Only walk up the given directory, if any.
|
// Only walk up the given directory, if any.
|
||||||
|
@ -329,17 +550,6 @@ impl ProjectWorkspace {
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let mut workspace_members = BTreeMap::new();
|
|
||||||
// The current project is always a workspace member, especially in a single project
|
|
||||||
// workspace.
|
|
||||||
workspace_members.insert(
|
|
||||||
project_name.clone(),
|
|
||||||
WorkspaceMember {
|
|
||||||
root: project_path.clone(),
|
|
||||||
pyproject_toml: project.clone(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if the current project is also an explicit workspace root.
|
// Check if the current project is also an explicit workspace root.
|
||||||
let mut workspace = project
|
let mut workspace = project
|
||||||
.tool
|
.tool
|
||||||
|
@ -359,13 +569,20 @@ impl ProjectWorkspace {
|
||||||
// The project isn't an explicit workspace root, but there's also no workspace root
|
// The project isn't an explicit workspace root, but there's also no workspace root
|
||||||
// above it, so the project is an implicit workspace root identical to the project root.
|
// above it, so the project is an implicit workspace root identical to the project root.
|
||||||
debug!("No workspace root found, using project root");
|
debug!("No workspace root found, using project root");
|
||||||
|
let current_project_as_members = BTreeMap::from_iter([(
|
||||||
|
project_name.clone(),
|
||||||
|
WorkspaceMember {
|
||||||
|
root: project_path.clone(),
|
||||||
|
pyproject_toml: project.clone(),
|
||||||
|
},
|
||||||
|
)]);
|
||||||
return Ok(Self {
|
return Ok(Self {
|
||||||
project_root: project_path.clone(),
|
project_root: project_path.clone(),
|
||||||
project_name,
|
project_name: project_name.clone(),
|
||||||
extras,
|
extras,
|
||||||
workspace: Workspace {
|
workspace: Workspace {
|
||||||
root: project_path,
|
root: project_path.clone(),
|
||||||
packages: workspace_members,
|
packages: current_project_as_members,
|
||||||
// There may be package sources, but we don't need to duplicate them into the
|
// There may be package sources, but we don't need to duplicate them into the
|
||||||
// workspace sources.
|
// workspace sources.
|
||||||
sources: BTreeMap::default(),
|
sources: BTreeMap::default(),
|
||||||
|
@ -377,79 +594,21 @@ impl ProjectWorkspace {
|
||||||
"Found workspace root: `{}`",
|
"Found workspace root: `{}`",
|
||||||
workspace_root.simplified_display()
|
workspace_root.simplified_display()
|
||||||
);
|
);
|
||||||
if workspace_root != project_path {
|
|
||||||
let pyproject_path = workspace_root.join("pyproject.toml");
|
|
||||||
let contents = fs_err::read_to_string(&pyproject_path)?;
|
|
||||||
let pyproject_toml = toml::from_str(&contents)
|
|
||||||
.map_err(|err| WorkspaceError::Toml(pyproject_path, Box::new(err)))?;
|
|
||||||
|
|
||||||
if let Some(project) = &workspace_pyproject_toml.project {
|
let workspace = Workspace::collect_members(
|
||||||
workspace_members.insert(
|
workspace_root,
|
||||||
project.name.clone(),
|
workspace_definition,
|
||||||
WorkspaceMember {
|
workspace_pyproject_toml,
|
||||||
root: workspace_root.clone(),
|
Some((project_name.clone(), project_path.clone(), project.clone())),
|
||||||
pyproject_toml,
|
stop_discovery_at,
|
||||||
},
|
)
|
||||||
);
|
.await?;
|
||||||
};
|
|
||||||
}
|
|
||||||
let mut seen = FxHashSet::default();
|
|
||||||
for member_glob in workspace_definition.members.unwrap_or_default() {
|
|
||||||
let absolute_glob = workspace_root
|
|
||||||
.simplified()
|
|
||||||
.join(member_glob.as_str())
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string();
|
|
||||||
for member_root in glob(&absolute_glob)
|
|
||||||
.map_err(|err| WorkspaceError::Pattern(absolute_glob.to_string(), err))?
|
|
||||||
{
|
|
||||||
let member_root = member_root
|
|
||||||
.map_err(|err| WorkspaceError::Glob(absolute_glob.to_string(), err))?;
|
|
||||||
// Avoid reading the file more than once.
|
|
||||||
if !seen.insert(member_root.clone()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let member_root = absolutize_path(&member_root)
|
|
||||||
.map_err(WorkspaceError::Normalize)?
|
|
||||||
.to_path_buf();
|
|
||||||
|
|
||||||
trace!("Processing workspace member {}", member_root.user_display());
|
|
||||||
// Read the member `pyproject.toml`.
|
|
||||||
let pyproject_path = member_root.join("pyproject.toml");
|
|
||||||
let contents = fs_err::read_to_string(&pyproject_path)?;
|
|
||||||
let pyproject_toml: PyProjectToml = toml::from_str(&contents)
|
|
||||||
.map_err(|err| WorkspaceError::Toml(pyproject_path, Box::new(err)))?;
|
|
||||||
|
|
||||||
// Extract the package name.
|
|
||||||
let Some(project) = pyproject_toml.project.clone() else {
|
|
||||||
return Err(WorkspaceError::MissingProject(member_root));
|
|
||||||
};
|
|
||||||
|
|
||||||
let member = WorkspaceMember {
|
|
||||||
root: member_root.clone(),
|
|
||||||
pyproject_toml,
|
|
||||||
};
|
|
||||||
workspace_members.insert(project.name, member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let workspace_sources = workspace_pyproject_toml
|
|
||||||
.tool
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|tool| tool.uv.as_ref())
|
|
||||||
.and_then(|uv| uv.sources.clone())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
check_nested_workspaces(&workspace_root, stop_discovery_at);
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
project_root: project_path.clone(),
|
project_root: project_path,
|
||||||
project_name,
|
project_name,
|
||||||
extras,
|
extras,
|
||||||
workspace: Workspace {
|
workspace,
|
||||||
root: workspace_root,
|
|
||||||
packages: workspace_members,
|
|
||||||
sources: workspace_sources,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -669,13 +828,12 @@ fn is_excluded_from_workspace(
|
||||||
#[cfg(unix)] // Avoid path escaping for the unit tests
|
#[cfg(unix)] // Avoid path escaping for the unit tests
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use insta::assert_json_snapshot;
|
use insta::assert_json_snapshot;
|
||||||
|
|
||||||
use crate::workspace::ProjectWorkspace;
|
use crate::workspace::ProjectWorkspace;
|
||||||
|
|
||||||
async fn workspace_test(folder: impl AsRef<Path>) -> (ProjectWorkspace, String) {
|
async fn workspace_test(folder: &str) -> (ProjectWorkspace, String) {
|
||||||
let root_dir = env::current_dir()
|
let root_dir = env::current_dir()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.parent()
|
.parent()
|
||||||
|
@ -684,7 +842,7 @@ mod tests {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.join("scripts")
|
.join("scripts")
|
||||||
.join("workspaces");
|
.join("workspaces");
|
||||||
let project = ProjectWorkspace::discover(root_dir.join(folder), None)
|
let project = ProjectWorkspace::discover(&root_dir.join(folder), None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let root_escaped = regex::escape(root_dir.to_string_lossy().as_ref());
|
let root_escaped = regex::escape(root_dir.to_string_lossy().as_ref());
|
||||||
|
|
|
@ -38,6 +38,7 @@ fs-err = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
rayon = { workspace = true }
|
rayon = { workspace = true }
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
|
same-file = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|
|
@ -113,16 +113,18 @@ impl<'a> Planner<'a> {
|
||||||
NoBinary::Packages(packages) => packages.contains(&requirement.name),
|
NoBinary::Packages(packages) => packages.contains(&requirement.name),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let installed_dists = site_packages.remove_packages(&requirement.name);
|
||||||
|
|
||||||
if reinstall {
|
if reinstall {
|
||||||
let installed_dists = site_packages.remove_packages(&requirement.name);
|
|
||||||
reinstalls.extend(installed_dists);
|
reinstalls.extend(installed_dists);
|
||||||
} else {
|
} else {
|
||||||
let installed_dists = site_packages.remove_packages(&requirement.name);
|
|
||||||
match installed_dists.as_slice() {
|
match installed_dists.as_slice() {
|
||||||
[] => {}
|
[] => {}
|
||||||
[distribution] => {
|
[distribution] => {
|
||||||
match RequirementSatisfaction::check(distribution, &requirement.source)? {
|
match RequirementSatisfaction::check(distribution, &requirement.source)? {
|
||||||
RequirementSatisfaction::Mismatch => {}
|
RequirementSatisfaction::Mismatch => {
|
||||||
|
debug!("Requirement installed, but mismatched: {distribution:?}");
|
||||||
|
}
|
||||||
RequirementSatisfaction::Satisfied => {
|
RequirementSatisfaction::Satisfied => {
|
||||||
debug!("Requirement already installed: {distribution}");
|
debug!("Requirement already installed: {distribution}");
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -2,8 +2,10 @@ use std::fmt::Debug;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use same_file::is_same_file;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use cache_key::{CanonicalUrl, RepositoryUrl};
|
use cache_key::{CanonicalUrl, RepositoryUrl};
|
||||||
use distribution_types::{InstalledDirectUrlDist, InstalledDist};
|
use distribution_types::{InstalledDirectUrlDist, InstalledDist};
|
||||||
|
@ -147,8 +149,8 @@ impl RequirementSatisfaction {
|
||||||
Ok(Self::Satisfied)
|
Ok(Self::Satisfied)
|
||||||
}
|
}
|
||||||
RequirementSource::Path {
|
RequirementSource::Path {
|
||||||
path,
|
url: _,
|
||||||
url: requested_url,
|
path: requested_path,
|
||||||
editable: requested_editable,
|
editable: requested_editable,
|
||||||
} => {
|
} => {
|
||||||
let InstalledDist::Url(InstalledDirectUrlDist { direct_url, .. }) = &distribution
|
let InstalledDist::Url(InstalledDirectUrlDist { direct_url, .. }) = &distribution
|
||||||
|
@ -175,24 +177,34 @@ impl RequirementSatisfaction {
|
||||||
return Ok(Self::Mismatch);
|
return Ok(Self::Mismatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !CanonicalUrl::parse(installed_url)
|
let Some(installed_path) = Url::parse(installed_url)
|
||||||
.is_ok_and(|installed_url| installed_url == CanonicalUrl::new(requested_url))
|
.ok()
|
||||||
|
.and_then(|url| url.to_file_path().ok())
|
||||||
|
else {
|
||||||
|
return Ok(Self::Mismatch);
|
||||||
|
};
|
||||||
|
|
||||||
|
if !(*requested_path == installed_path
|
||||||
|
|| is_same_file(requested_path, &installed_path).unwrap_or(false))
|
||||||
{
|
{
|
||||||
trace!(
|
trace!(
|
||||||
"URL mismatch: {:?} vs. {:?}",
|
"Path mismatch: {:?} vs. {:?}",
|
||||||
CanonicalUrl::parse(installed_url),
|
requested_path,
|
||||||
CanonicalUrl::new(requested_url)
|
installed_path
|
||||||
);
|
);
|
||||||
return Ok(Self::Mismatch);
|
return Ok(Self::Satisfied);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ArchiveTimestamp::up_to_date_with(path, ArchiveTarget::Install(distribution))? {
|
if !ArchiveTimestamp::up_to_date_with(
|
||||||
|
requested_path,
|
||||||
|
ArchiveTarget::Install(distribution),
|
||||||
|
)? {
|
||||||
trace!("Out of date");
|
trace!("Out of date");
|
||||||
return Ok(Self::OutOfDate);
|
return Ok(Self::OutOfDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Does the package have dynamic metadata?
|
// Does the package have dynamic metadata?
|
||||||
if is_dynamic(path) {
|
if is_dynamic(requested_path) {
|
||||||
return Ok(Self::Dynamic);
|
return Ok(Self::Dynamic);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1831,6 +1831,10 @@ pub(crate) struct RunArgs {
|
||||||
/// format (e.g., `2006-12-02`).
|
/// format (e.g., `2006-12-02`).
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub(crate) exclude_newer: Option<ExcludeNewer>,
|
pub(crate) exclude_newer: Option<ExcludeNewer>,
|
||||||
|
|
||||||
|
/// Run the command in a different package in the workspace.
|
||||||
|
#[arg(long, conflicts_with = "isolated")]
|
||||||
|
pub(crate) package: Option<PackageName>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
|
@ -36,7 +36,7 @@ pub(crate) async fn lock(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the project requirements.
|
// Find the project requirements.
|
||||||
let project = ProjectWorkspace::discover(std::env::current_dir()?, None).await?;
|
let project = ProjectWorkspace::discover(&std::env::current_dir()?, None).await?;
|
||||||
|
|
||||||
// Discover or create the virtual environment.
|
// Discover or create the virtual environment.
|
||||||
let venv = project::init_environment(&project, preview, cache, printer)?;
|
let venv = project::init_environment(&project, preview, cache, printer)?;
|
||||||
|
|
|
@ -10,8 +10,9 @@ use tracing::debug;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::Connectivity;
|
use uv_client::Connectivity;
|
||||||
use uv_configuration::{ExtrasSpecification, PreviewMode, Upgrade};
|
use uv_configuration::{ExtrasSpecification, PreviewMode, Upgrade};
|
||||||
use uv_distribution::ProjectWorkspace;
|
use uv_distribution::{ProjectWorkspace, Workspace};
|
||||||
use uv_interpreter::{PythonEnvironment, SystemPython};
|
use uv_interpreter::{PythonEnvironment, SystemPython};
|
||||||
|
use uv_normalize::PackageName;
|
||||||
use uv_requirements::RequirementsSource;
|
use uv_requirements::RequirementsSource;
|
||||||
use uv_resolver::ExcludeNewer;
|
use uv_resolver::ExcludeNewer;
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
|
@ -29,6 +30,7 @@ pub(crate) async fn run(
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
upgrade: Upgrade,
|
upgrade: Upgrade,
|
||||||
exclude_newer: Option<ExcludeNewer>,
|
exclude_newer: Option<ExcludeNewer>,
|
||||||
|
package: Option<PackageName>,
|
||||||
isolated: bool,
|
isolated: bool,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
|
@ -41,11 +43,21 @@ pub(crate) async fn run(
|
||||||
|
|
||||||
// Discover and sync the project.
|
// Discover and sync the project.
|
||||||
let project_env = if isolated {
|
let project_env = if isolated {
|
||||||
|
// package is `None`, isolated and package are marked as conflicting in clap.
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
debug!("Syncing project environment.");
|
debug!("Syncing project environment.");
|
||||||
|
|
||||||
let project = ProjectWorkspace::discover(std::env::current_dir()?, None).await?;
|
let project = if let Some(package) = package {
|
||||||
|
// We need a workspace, but we don't need to have a current package, we can be e.g. in
|
||||||
|
// the root of a virtual workspace and then switch into the selected package.
|
||||||
|
Workspace::discover(&std::env::current_dir()?, None)
|
||||||
|
.await?
|
||||||
|
.with_current_project(package.clone())
|
||||||
|
.with_context(|| format!("Package `{package}` not found in workspace"))?
|
||||||
|
} else {
|
||||||
|
ProjectWorkspace::discover(&std::env::current_dir()?, None).await?
|
||||||
|
};
|
||||||
let venv = project::init_environment(&project, preview, cache, printer)?;
|
let venv = project::init_environment(&project, preview, cache, printer)?;
|
||||||
|
|
||||||
// Lock and sync the environment.
|
// Lock and sync the environment.
|
||||||
|
|
|
@ -35,7 +35,7 @@ pub(crate) async fn sync(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the project requirements.
|
// Find the project requirements.
|
||||||
let project = ProjectWorkspace::discover(std::env::current_dir()?, None).await?;
|
let project = ProjectWorkspace::discover(&std::env::current_dir()?, None).await?;
|
||||||
|
|
||||||
// Discover or create the virtual environment.
|
// Discover or create the virtual environment.
|
||||||
let venv = project::init_environment(&project, preview, cache, printer)?;
|
let venv = project::init_environment(&project, preview, cache, printer)?;
|
||||||
|
|
|
@ -580,6 +580,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
args.python,
|
args.python,
|
||||||
args.upgrade,
|
args.upgrade,
|
||||||
args.exclude_newer,
|
args.exclude_newer,
|
||||||
|
args.package,
|
||||||
globals.isolated,
|
globals.isolated,
|
||||||
globals.preview,
|
globals.preview,
|
||||||
globals.connectivity,
|
globals.connectivity,
|
||||||
|
|
|
@ -105,6 +105,7 @@ pub(crate) struct RunSettings {
|
||||||
pub(crate) refresh: Refresh,
|
pub(crate) refresh: Refresh,
|
||||||
pub(crate) upgrade: Upgrade,
|
pub(crate) upgrade: Upgrade,
|
||||||
pub(crate) exclude_newer: Option<ExcludeNewer>,
|
pub(crate) exclude_newer: Option<ExcludeNewer>,
|
||||||
|
pub(crate) package: Option<PackageName>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RunSettings {
|
impl RunSettings {
|
||||||
|
@ -126,6 +127,7 @@ impl RunSettings {
|
||||||
upgrade_package,
|
upgrade_package,
|
||||||
python,
|
python,
|
||||||
exclude_newer,
|
exclude_newer,
|
||||||
|
package,
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
@ -140,6 +142,7 @@ impl RunSettings {
|
||||||
with,
|
with,
|
||||||
python,
|
python,
|
||||||
exclude_newer,
|
exclude_newer,
|
||||||
|
package,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -276,6 +276,10 @@ impl TestContext {
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn interpreter(&self) -> PathBuf {
|
||||||
|
venv_to_interpreter(&self.venv)
|
||||||
|
}
|
||||||
|
|
||||||
/// Run the given python code and check whether it succeeds.
|
/// Run the given python code and check whether it succeeds.
|
||||||
pub fn assert_command(&self, command: &str) -> Assert {
|
pub fn assert_command(&self, command: &str) -> Assert {
|
||||||
std::process::Command::new(venv_to_interpreter(&self.venv))
|
std::process::Command::new(venv_to_interpreter(&self.venv))
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
use crate::common::{get_bin, uv_snapshot, TestContext, EXCLUDE_NEWER};
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::common::{copy_dir_all, get_bin, uv_snapshot, TestContext, EXCLUDE_NEWER};
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
|
@ -10,8 +13,8 @@ mod common;
|
||||||
/// The goal of the workspace tests is to resolve local workspace packages correctly. We add some
|
/// The goal of the workspace tests is to resolve local workspace packages correctly. We add some
|
||||||
/// non-workspace dependencies to ensure that transitive non-workspace dependencies are also
|
/// non-workspace dependencies to ensure that transitive non-workspace dependencies are also
|
||||||
/// correctly resolved.
|
/// correctly resolved.
|
||||||
pub fn install_workspace(context: &TestContext) -> std::process::Command {
|
fn install_workspace(context: &TestContext) -> Command {
|
||||||
let mut command = std::process::Command::new(get_bin());
|
let mut command = Command::new(get_bin());
|
||||||
command
|
command
|
||||||
.arg("pip")
|
.arg("pip")
|
||||||
.arg("install")
|
.arg("install")
|
||||||
|
@ -34,6 +37,28 @@ pub fn install_workspace(context: &TestContext) -> std::process::Command {
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A `uv run` command.
|
||||||
|
fn run_workspace(context: &TestContext) -> Command {
|
||||||
|
let mut command = Command::new(get_bin());
|
||||||
|
command
|
||||||
|
.arg("run")
|
||||||
|
.arg("--preview")
|
||||||
|
.arg("--cache-dir")
|
||||||
|
.arg(context.cache_dir.path())
|
||||||
|
.arg("--python")
|
||||||
|
.arg(context.interpreter())
|
||||||
|
.arg("--exclude-newer")
|
||||||
|
.arg(EXCLUDE_NEWER)
|
||||||
|
.env("UV_NO_WRAP", "1");
|
||||||
|
|
||||||
|
if cfg!(all(windows, debug_assertions)) {
|
||||||
|
// TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the
|
||||||
|
// default windows stack of 1MB
|
||||||
|
command.env("UV_STACK_SIZE", (4 * 1024 * 1024).to_string());
|
||||||
|
}
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
fn workspaces_dir() -> PathBuf {
|
fn workspaces_dir() -> PathBuf {
|
||||||
env::current_dir()
|
env::current_dir()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -341,3 +366,142 @@ fn test_albatross_virtual_workspace() {
|
||||||
|
|
||||||
context.assert_file(current_dir.join("check_installed_bird_feeder.py"));
|
context.assert_file(current_dir.join("check_installed_bird_feeder.py"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check that `uv run --package` works in a virtual workspace.
|
||||||
|
#[test]
|
||||||
|
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_all(
|
||||||
|
workspaces_dir().join("albatross-virtual-workspace"),
|
||||||
|
&work_dir,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// TODO(konsti): `--python` is being ignored atm, so we need to create the correct venv
|
||||||
|
// ourselves and add the output filters.
|
||||||
|
let venv = work_dir.join(".venv");
|
||||||
|
assert_cmd::Command::new(get_bin())
|
||||||
|
.arg("venv")
|
||||||
|
.arg("-p")
|
||||||
|
.arg(context.interpreter())
|
||||||
|
.arg(&venv)
|
||||||
|
.assert();
|
||||||
|
|
||||||
|
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, run_workspace(&context)
|
||||||
|
.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 -----
|
||||||
|
Resolved 5 packages in [TIME]
|
||||||
|
Downloaded 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(), run_workspace(&context)
|
||||||
|
.arg("--package")
|
||||||
|
.arg("albatross")
|
||||||
|
.arg("packages/albatross/check_installed_albatross.py")
|
||||||
|
.current_dir(&work_dir), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Success
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 7 packages in [TIME]
|
||||||
|
Downloaded 2 packages in [TIME]
|
||||||
|
Installed 2 packages in [TIME]
|
||||||
|
+ albatross==0.1.0 (from file://[TEMP_DIR]/albatross-virtual-workspace/packages/albatross)
|
||||||
|
+ tqdm==4.66.2
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check that `uv run --package` works in a root workspace.
|
||||||
|
#[test]
|
||||||
|
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_all(workspaces_dir().join("albatross-root-workspace"), &work_dir)?;
|
||||||
|
|
||||||
|
// TODO(konsti): `--python` is being ignored atm, so we need to create the correct venv
|
||||||
|
// ourselves and add the output filters.
|
||||||
|
let venv = work_dir.join(".venv");
|
||||||
|
assert_cmd::Command::new(get_bin())
|
||||||
|
.arg("venv")
|
||||||
|
.arg("-p")
|
||||||
|
.arg(context.interpreter())
|
||||||
|
.arg(&venv)
|
||||||
|
.assert();
|
||||||
|
|
||||||
|
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, run_workspace(&context)
|
||||||
|
.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 -----
|
||||||
|
Resolved 5 packages in [TIME]
|
||||||
|
Downloaded 5 packages in [TIME]
|
||||||
|
Installed 5 packages in [TIME]
|
||||||
|
+ anyio==4.3.0
|
||||||
|
+ bird-feeder==1.0.0 (from file://[TEMP_DIR]/albatross-root-workspace/packages/bird-feeder)
|
||||||
|
+ idna==3.6
|
||||||
|
+ seeds==1.0.0 (from file://[TEMP_DIR]/albatross-root-workspace/packages/seeds)
|
||||||
|
+ sniffio==1.3.1
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), run_workspace(&context)
|
||||||
|
.arg("--package")
|
||||||
|
.arg("albatross")
|
||||||
|
.arg("check_installed_albatross.py")
|
||||||
|
.current_dir(&work_dir), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Success
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 7 packages in [TIME]
|
||||||
|
Downloaded 2 packages in [TIME]
|
||||||
|
Installed 2 packages in [TIME]
|
||||||
|
+ albatross==0.1.0 (from file://[TEMP_DIR]/albatross-root-workspace)
|
||||||
|
+ tqdm==4.66.2
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue