mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Discover workspaces without using them in resolution (#3585)
Add minimal support for workspace discovery, only used for determining paths in the bluejay commands. We can now discover the workspace structure, namely that the `pyproject.toml` of a package belongs to a workspace `pyproject.toml` with members and exclusion. The globbing logic is inspired by cargo. We don't resolve `workspace = true` metadata declarations yet.
This commit is contained in:
parent
5205165d42
commit
2ffd453003
43 changed files with 1007 additions and 151 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4955,6 +4955,7 @@ dependencies = [
|
||||||
"pep440_rs",
|
"pep440_rs",
|
||||||
"pep508_rs",
|
"pep508_rs",
|
||||||
"pypi-types",
|
"pypi-types",
|
||||||
|
"regex",
|
||||||
"requirements-txt",
|
"requirements-txt",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"schemars",
|
"schemars",
|
||||||
|
|
|
@ -50,7 +50,8 @@ schemars = ["dep:schemars"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
indoc = "2.0.5"
|
indoc = "2.0.5"
|
||||||
insta = "1.38.0"
|
insta = { version = "1.38.0", features = ["filters", "redactions", "json"] }
|
||||||
|
regex = { workspace = true }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -3,6 +3,7 @@ pub use crate::source_tree::*;
|
||||||
pub use crate::sources::*;
|
pub use crate::sources::*;
|
||||||
pub use crate::specification::*;
|
pub use crate::specification::*;
|
||||||
pub use crate::unnamed::*;
|
pub use crate::unnamed::*;
|
||||||
|
pub use crate::workspace::*;
|
||||||
|
|
||||||
mod confirm;
|
mod confirm;
|
||||||
mod lookahead;
|
mod lookahead;
|
||||||
|
@ -12,3 +13,4 @@ mod sources;
|
||||||
mod specification;
|
mod specification;
|
||||||
mod unnamed;
|
mod unnamed;
|
||||||
pub mod upgrade;
|
pub mod upgrade;
|
||||||
|
mod workspace;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//! Reads the following fields from from `pyproject.toml`:
|
//! Reads the following fields from `pyproject.toml`:
|
||||||
//!
|
//!
|
||||||
//! * `project.{dependencies,optional-dependencies}`
|
//! * `project.{dependencies,optional-dependencies}`
|
||||||
//! * `tool.uv.sources`
|
//! * `tool.uv.sources`
|
||||||
|
@ -6,7 +6,7 @@
|
||||||
//!
|
//!
|
||||||
//! Then lowers them into a dependency specification.
|
//! Then lowers them into a dependency specification.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::BTreeMap;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
@ -110,7 +110,7 @@ pub struct Tool {
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct ToolUv {
|
pub struct ToolUv {
|
||||||
pub sources: Option<HashMap<PackageName, Source>>,
|
pub sources: Option<BTreeMap<PackageName, Source>>,
|
||||||
pub workspace: Option<ToolUvWorkspace>,
|
pub workspace: Option<ToolUvWorkspace>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,8 +238,8 @@ impl Pep621Metadata {
|
||||||
extras: &ExtrasSpecification,
|
extras: &ExtrasSpecification,
|
||||||
pyproject_path: &Path,
|
pyproject_path: &Path,
|
||||||
project_dir: &Path,
|
project_dir: &Path,
|
||||||
workspace_sources: &HashMap<PackageName, Source>,
|
workspace_sources: &BTreeMap<PackageName, Source>,
|
||||||
workspace_packages: &HashMap<PackageName, String>,
|
workspace_packages: &BTreeMap<PackageName, String>,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
) -> Result<Option<Self>, Pep621Error> {
|
) -> Result<Option<Self>, Pep621Error> {
|
||||||
let project_sources = pyproject
|
let project_sources = pyproject
|
||||||
|
@ -323,9 +323,9 @@ pub(crate) fn lower_requirements(
|
||||||
pyproject_path: &Path,
|
pyproject_path: &Path,
|
||||||
project_name: &PackageName,
|
project_name: &PackageName,
|
||||||
project_dir: &Path,
|
project_dir: &Path,
|
||||||
project_sources: &HashMap<PackageName, Source>,
|
project_sources: &BTreeMap<PackageName, Source>,
|
||||||
workspace_sources: &HashMap<PackageName, Source>,
|
workspace_sources: &BTreeMap<PackageName, Source>,
|
||||||
workspace_packages: &HashMap<PackageName, String>,
|
workspace_packages: &BTreeMap<PackageName, String>,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
) -> Result<Requirements, Pep621Error> {
|
) -> Result<Requirements, Pep621Error> {
|
||||||
let dependencies = dependencies
|
let dependencies = dependencies
|
||||||
|
@ -386,9 +386,9 @@ pub(crate) fn lower_requirement(
|
||||||
requirement: pep508_rs::Requirement,
|
requirement: pep508_rs::Requirement,
|
||||||
project_name: &PackageName,
|
project_name: &PackageName,
|
||||||
project_dir: &Path,
|
project_dir: &Path,
|
||||||
project_sources: &HashMap<PackageName, Source>,
|
project_sources: &BTreeMap<PackageName, Source>,
|
||||||
workspace_sources: &HashMap<PackageName, Source>,
|
workspace_sources: &BTreeMap<PackageName, Source>,
|
||||||
workspace_packages: &HashMap<PackageName, String>,
|
workspace_packages: &BTreeMap<PackageName, String>,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
) -> Result<Requirement, LoweringError> {
|
) -> Result<Requirement, LoweringError> {
|
||||||
let source = project_sources
|
let source = project_sources
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
@ -165,8 +165,8 @@ impl RequirementsSpecification {
|
||||||
.parent()
|
.parent()
|
||||||
.context("`pyproject.toml` has no parent directory")?;
|
.context("`pyproject.toml` has no parent directory")?;
|
||||||
|
|
||||||
let workspace_sources = HashMap::default();
|
let workspace_sources = BTreeMap::default();
|
||||||
let workspace_packages = HashMap::default();
|
let workspace_packages = BTreeMap::default();
|
||||||
match Pep621Metadata::try_from(
|
match Pep621Metadata::try_from(
|
||||||
pyproject,
|
pyproject,
|
||||||
extras,
|
extras,
|
||||||
|
|
705
crates/uv-requirements/src/workspace.rs
Normal file
705
crates/uv-requirements/src/workspace.rs
Normal file
|
@ -0,0 +1,705 @@
|
||||||
|
//! Resolve the current [`ProjectWorkspace`].
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use glob::{glob, GlobError, PatternError};
|
||||||
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
|
use uv_fs::Simplified;
|
||||||
|
use uv_normalize::PackageName;
|
||||||
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
|
use crate::pyproject::{PyProjectToml, Source, ToolUvWorkspace};
|
||||||
|
use crate::RequirementsSource;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum WorkspaceError {
|
||||||
|
#[error("No `pyproject.toml` found in current directory or any parent directory")]
|
||||||
|
MissingPyprojectToml,
|
||||||
|
#[error("Failed to find directories for glob: `{0}`")]
|
||||||
|
Pattern(String, #[source] PatternError),
|
||||||
|
#[error("Invalid glob in `tool.uv.workspace.members`: `{0}`")]
|
||||||
|
Glob(String, #[source] GlobError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("Failed to parse: `{}`", _0.user_display())]
|
||||||
|
Toml(PathBuf, #[source] toml::de::Error),
|
||||||
|
#[error("No `project` section found in: `{}`", _0.simplified_display())]
|
||||||
|
MissingProject(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A workspace, consisting of a root directory and members. See [`ProjectWorkspace`].
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(test, derive(serde::Serialize))]
|
||||||
|
pub struct Workspace {
|
||||||
|
/// 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.
|
||||||
|
root: PathBuf,
|
||||||
|
/// The members of the workspace.
|
||||||
|
packages: BTreeMap<PackageName, WorkspaceMember>,
|
||||||
|
/// The sources table from the workspace `pyproject.toml`. It is overridden by the project
|
||||||
|
/// sources.
|
||||||
|
sources: BTreeMap<PackageName, Source>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Workspace {
|
||||||
|
/// 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.
|
||||||
|
pub fn root(&self) -> &PathBuf {
|
||||||
|
&self.root
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The members of the workspace.
|
||||||
|
pub fn packages(&self) -> &BTreeMap<PackageName, WorkspaceMember> {
|
||||||
|
&self.packages
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sources table from the workspace `pyproject.toml`.
|
||||||
|
pub fn sources(&self) -> &BTreeMap<PackageName, Source> {
|
||||||
|
&self.sources
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A project in a workspace.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(test, derive(serde::Serialize))]
|
||||||
|
pub struct WorkspaceMember {
|
||||||
|
/// The path to the project root.
|
||||||
|
root: PathBuf,
|
||||||
|
/// The `pyproject.toml` of the project, found at `<root>/pyproject.toml`.
|
||||||
|
pyproject_toml: PyProjectToml,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkspaceMember {
|
||||||
|
/// The path to the project root.
|
||||||
|
pub fn root(&self) -> &PathBuf {
|
||||||
|
&self.root
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `pyproject.toml` of the project, found at `<root>/pyproject.toml`.
|
||||||
|
pub fn pyproject_toml(&self) -> &PyProjectToml {
|
||||||
|
&self.pyproject_toml
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current project and the workspace it is part of, with all of the workspace members.
|
||||||
|
///
|
||||||
|
/// # Structure
|
||||||
|
///
|
||||||
|
/// The workspace root is a directory with a `pyproject.toml`, all members need to be below that
|
||||||
|
/// directory. The workspace root defines members and exclusions. All packages below it must either
|
||||||
|
/// be a member or excluded. The workspace root can be a package itself or a virtual manifest.
|
||||||
|
///
|
||||||
|
/// For a simple single package project, the workspace root is implicitly the current project root
|
||||||
|
/// and the workspace has only this single member. Otherwise, a workspace root is declared through
|
||||||
|
/// a `tool.uv.workspace` section.
|
||||||
|
///
|
||||||
|
/// A workspace itself does not declare dependencies, instead one member is the current project used
|
||||||
|
/// as main requirement.
|
||||||
|
///
|
||||||
|
/// Each member is a directory with a `pyproject.toml` that contains a `[project]` section. Each
|
||||||
|
/// member is a Python package, with a name, a version and dependencies. Workspace members can
|
||||||
|
/// depend on other workspace members (`foo = { workspace = true }`). You can consider the
|
||||||
|
/// workspace another package source or index, similar to `--find-links`.
|
||||||
|
///
|
||||||
|
/// # Usage
|
||||||
|
///
|
||||||
|
/// There a two main usage patterns: A root package and helpers, and the flat workspace.
|
||||||
|
///
|
||||||
|
/// Root package and helpers:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// albatross
|
||||||
|
/// ├── packages
|
||||||
|
/// │ ├── provider_a
|
||||||
|
/// │ │ ├── pyproject.toml
|
||||||
|
/// │ │ └── src
|
||||||
|
/// │ │ └── provider_a
|
||||||
|
/// │ │ ├── __init__.py
|
||||||
|
/// │ │ └── foo.py
|
||||||
|
/// │ └── provider_b
|
||||||
|
/// │ ├── pyproject.toml
|
||||||
|
/// │ └── src
|
||||||
|
/// │ └── provider_b
|
||||||
|
/// │ ├── __init__.py
|
||||||
|
/// │ └── bar.py
|
||||||
|
/// ├── pyproject.toml
|
||||||
|
/// ├── Readme.md
|
||||||
|
/// ├── uv.lock
|
||||||
|
/// └── src
|
||||||
|
/// └── albatross
|
||||||
|
/// ├── __init__.py
|
||||||
|
/// └── main.py
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Flat workspace:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// albatross
|
||||||
|
/// ├── packages
|
||||||
|
/// │ ├── albatross
|
||||||
|
/// │ │ ├── pyproject.toml
|
||||||
|
/// │ │ └── src
|
||||||
|
/// │ │ └── albatross
|
||||||
|
/// │ │ ├── __init__.py
|
||||||
|
/// │ │ └── main.py
|
||||||
|
/// │ ├── provider_a
|
||||||
|
/// │ │ ├── pyproject.toml
|
||||||
|
/// │ │ └── src
|
||||||
|
/// │ │ └── provider_a
|
||||||
|
/// │ │ ├── __init__.py
|
||||||
|
/// │ │ └── foo.py
|
||||||
|
/// │ └── provider_b
|
||||||
|
/// │ ├── pyproject.toml
|
||||||
|
/// │ └── src
|
||||||
|
/// │ └── provider_b
|
||||||
|
/// │ ├── __init__.py
|
||||||
|
/// │ └── bar.py
|
||||||
|
/// ├── pyproject.toml
|
||||||
|
/// ├── Readme.md
|
||||||
|
/// └── uv.lock
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(test, derive(serde::Serialize))]
|
||||||
|
pub struct ProjectWorkspace {
|
||||||
|
/// The path to the project root.
|
||||||
|
project_root: PathBuf,
|
||||||
|
/// The name of the package.
|
||||||
|
project_name: PackageName,
|
||||||
|
/// The workspace the project is part of.
|
||||||
|
workspace: Workspace,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectWorkspace {
|
||||||
|
/// Find the current project and workspace.
|
||||||
|
pub fn discover(path: impl AsRef<Path>) -> Result<Self, WorkspaceError> {
|
||||||
|
let Some(project_root) = path
|
||||||
|
.as_ref()
|
||||||
|
.ancestors()
|
||||||
|
.find(|path| path.join("pyproject.toml").is_file())
|
||||||
|
else {
|
||||||
|
return Err(WorkspaceError::MissingPyprojectToml);
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Found project root: `{}`",
|
||||||
|
project_root.simplified_display()
|
||||||
|
);
|
||||||
|
|
||||||
|
Self::from_project_root(project_root)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The directory containing the closest `pyproject.toml`, defining the current project.
|
||||||
|
pub fn project_root(&self) -> &Path {
|
||||||
|
&self.project_root
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The name of the current project.
|
||||||
|
pub fn project_name(&self) -> &PackageName {
|
||||||
|
&self.project_name
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The workspace definition.
|
||||||
|
pub fn workspace(&self) -> &Workspace {
|
||||||
|
&self.workspace
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the requirements for the project.
|
||||||
|
pub fn requirements(&self) -> Vec<RequirementsSource> {
|
||||||
|
vec![
|
||||||
|
RequirementsSource::from_requirements_file(self.project_root.join("pyproject.toml")),
|
||||||
|
RequirementsSource::from_source_tree(self.project_root.clone()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_project_root(path: &Path) -> Result<Self, WorkspaceError> {
|
||||||
|
let pyproject_path = path.join("pyproject.toml");
|
||||||
|
|
||||||
|
// Read the `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.clone(), err))?;
|
||||||
|
|
||||||
|
// Extract the `[project]` metadata.
|
||||||
|
let Some(project) = pyproject_toml.project.clone() else {
|
||||||
|
return Err(WorkspaceError::MissingProject(pyproject_path));
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::from_project(path.to_path_buf(), &pyproject_toml, project.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the workspace for a project.
|
||||||
|
fn from_project(
|
||||||
|
project_path: PathBuf,
|
||||||
|
project: &PyProjectToml,
|
||||||
|
project_name: PackageName,
|
||||||
|
) -> Result<Self, WorkspaceError> {
|
||||||
|
let mut workspace = project
|
||||||
|
.tool
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|tool| tool.uv.as_ref())
|
||||||
|
.and_then(|uv| uv.workspace.as_ref())
|
||||||
|
.map(|workspace| (project_path.clone(), workspace.clone(), project.clone()));
|
||||||
|
|
||||||
|
if workspace.is_none() {
|
||||||
|
workspace = find_workspace(&project_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut workspace_members = BTreeMap::new();
|
||||||
|
workspace_members.insert(
|
||||||
|
project_name.clone(),
|
||||||
|
WorkspaceMember {
|
||||||
|
root: project_path.clone(),
|
||||||
|
pyproject_toml: project.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let Some((workspace_root, workspace_definition, project_in_workspace_root)) = workspace
|
||||||
|
else {
|
||||||
|
// The project and the workspace root are identical
|
||||||
|
debug!("No workspace root found, using project root");
|
||||||
|
return Ok(Self {
|
||||||
|
project_root: project_path.clone(),
|
||||||
|
project_name,
|
||||||
|
workspace: Workspace {
|
||||||
|
root: project_path,
|
||||||
|
packages: workspace_members,
|
||||||
|
// There may be package sources, but we don't need to duplicate them into the
|
||||||
|
// workspace sources.
|
||||||
|
sources: BTreeMap::default(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Found workspace root: `{}`",
|
||||||
|
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, err))?;
|
||||||
|
|
||||||
|
if let Some(project) = &project_in_workspace_root.project {
|
||||||
|
workspace_members.insert(
|
||||||
|
project.name.clone(),
|
||||||
|
WorkspaceMember {
|
||||||
|
root: workspace_root.clone(),
|
||||||
|
pyproject_toml,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for member_glob in workspace_definition.members.unwrap_or_default() {
|
||||||
|
let absolute_glob = workspace_root
|
||||||
|
.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))?
|
||||||
|
{
|
||||||
|
// TODO(konsti): Filter already seen.
|
||||||
|
let member_root = member_root
|
||||||
|
.map_err(|err| WorkspaceError::Glob(absolute_glob.to_string(), err))?;
|
||||||
|
// Read the `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, err))?;
|
||||||
|
|
||||||
|
// Extract the package name.
|
||||||
|
let Some(project) = pyproject_toml.project.clone() else {
|
||||||
|
return Err(WorkspaceError::MissingProject(member_root));
|
||||||
|
};
|
||||||
|
|
||||||
|
let pyproject_toml = workspace_root.join("pyproject.toml");
|
||||||
|
let contents = fs_err::read_to_string(&pyproject_toml)?;
|
||||||
|
let pyproject_toml = toml::from_str(&contents)
|
||||||
|
.map_err(|err| WorkspaceError::Toml(pyproject_toml, err))?;
|
||||||
|
let member = WorkspaceMember {
|
||||||
|
root: member_root.clone(),
|
||||||
|
pyproject_toml,
|
||||||
|
};
|
||||||
|
workspace_members.insert(project.name, member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let workspace_sources = project_in_workspace_root
|
||||||
|
.tool
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|tool| tool.uv.as_ref())
|
||||||
|
.and_then(|uv| uv.sources.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
check_nested_workspaces(&workspace_root);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
project_root: project_path.clone(),
|
||||||
|
project_name,
|
||||||
|
workspace: Workspace {
|
||||||
|
root: workspace_root,
|
||||||
|
packages: workspace_members,
|
||||||
|
sources: workspace_sources,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the workspace root above the current project, if any.
|
||||||
|
fn find_workspace(
|
||||||
|
project_root: &Path,
|
||||||
|
) -> Result<Option<(PathBuf, ToolUvWorkspace, PyProjectToml)>, WorkspaceError> {
|
||||||
|
// Skip 1 to ignore the current project itself.
|
||||||
|
for workspace_root in project_root.ancestors().skip(1) {
|
||||||
|
let pyproject_path = workspace_root.join("pyproject.toml");
|
||||||
|
if !pyproject_path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
trace!(
|
||||||
|
"Found pyproject.toml: {}",
|
||||||
|
pyproject_path.simplified_display()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Read the `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.clone(), err))?;
|
||||||
|
|
||||||
|
return if let Some(workspace) = pyproject_toml
|
||||||
|
.tool
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|tool| tool.uv.as_ref())
|
||||||
|
.and_then(|uv| uv.workspace.as_ref())
|
||||||
|
{
|
||||||
|
if is_excluded_from_workspace(project_root, workspace_root, workspace)? {
|
||||||
|
debug!(
|
||||||
|
"Found workspace root `{}`, but project is excluded.",
|
||||||
|
workspace_root.simplified_display()
|
||||||
|
);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We found a workspace root.
|
||||||
|
Ok(Some((
|
||||||
|
workspace_root.to_path_buf(),
|
||||||
|
workspace.clone(),
|
||||||
|
pyproject_toml,
|
||||||
|
)))
|
||||||
|
} else if pyproject_toml.project.is_some() {
|
||||||
|
// We're in a directory of another project, e.g. tests or examples.
|
||||||
|
// Example:
|
||||||
|
// ```
|
||||||
|
// albatross
|
||||||
|
// ├── examples
|
||||||
|
// │ └── bird-feeder [CURRENT DIRECTORY]
|
||||||
|
// │ ├── pyproject.toml
|
||||||
|
// │ └── src
|
||||||
|
// │ └── bird_feeder
|
||||||
|
// │ └── __init__.py
|
||||||
|
// ├── pyproject.toml
|
||||||
|
// └── src
|
||||||
|
// └── albatross
|
||||||
|
// └── __init__.py
|
||||||
|
// ```
|
||||||
|
// The current project is the example (non-workspace) `bird-feeder` in `albatross`,
|
||||||
|
// we ignore all `albatross` is doing and any potential workspace it might be
|
||||||
|
// contained in.
|
||||||
|
debug!(
|
||||||
|
"Project is contained in non-workspace project: `{}`",
|
||||||
|
workspace_root.simplified_display()
|
||||||
|
);
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
// We require that a `project.toml` file either declares a workspace or a project.
|
||||||
|
warn_user!(
|
||||||
|
"pyproject.toml does not contain `project` table: `{}`",
|
||||||
|
workspace_root.simplified_display()
|
||||||
|
);
|
||||||
|
Ok(None)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Warn when the valid workspace is included in another workspace.
|
||||||
|
fn check_nested_workspaces(inner_workspace_root: &Path) {
|
||||||
|
for outer_workspace_root in inner_workspace_root.ancestors().skip(1) {
|
||||||
|
let pyproject_toml_path = outer_workspace_root.join("pyproject.toml");
|
||||||
|
if !pyproject_toml_path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let contents = match fs_err::read_to_string(&pyproject_toml_path) {
|
||||||
|
Ok(contents) => contents,
|
||||||
|
Err(err) => {
|
||||||
|
warn_user!(
|
||||||
|
"Unreadable pyproject.toml `{}`: {}",
|
||||||
|
pyproject_toml_path.user_display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let pyproject_toml: PyProjectToml = match toml::from_str(&contents) {
|
||||||
|
Ok(contents) => contents,
|
||||||
|
Err(err) => {
|
||||||
|
warn_user!(
|
||||||
|
"Invalid pyproject.toml `{}`: {}",
|
||||||
|
pyproject_toml_path.user_display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(workspace) = pyproject_toml
|
||||||
|
.tool
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|tool| tool.uv.as_ref())
|
||||||
|
.and_then(|uv| uv.workspace.as_ref())
|
||||||
|
{
|
||||||
|
let is_excluded = match is_excluded_from_workspace(
|
||||||
|
inner_workspace_root,
|
||||||
|
outer_workspace_root,
|
||||||
|
workspace,
|
||||||
|
) {
|
||||||
|
Ok(contents) => contents,
|
||||||
|
Err(err) => {
|
||||||
|
warn_user!(
|
||||||
|
"Invalid pyproject.toml `{}`: {}",
|
||||||
|
pyproject_toml_path.user_display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !is_excluded {
|
||||||
|
warn_user!(
|
||||||
|
"Outer workspace including existing workspace, nested workspaces are not supported: `{}`",
|
||||||
|
pyproject_toml_path.user_display(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're in the examples or tests of another project (not a workspace), this is fine.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if we're in the `tool.uv.workspace.excluded` of a workspace.
|
||||||
|
fn is_excluded_from_workspace(
|
||||||
|
project_path: &Path,
|
||||||
|
workspace_root: &Path,
|
||||||
|
workspace: &ToolUvWorkspace,
|
||||||
|
) -> Result<bool, WorkspaceError> {
|
||||||
|
for exclude_glob in workspace.exclude.iter().flatten() {
|
||||||
|
let absolute_glob = workspace_root
|
||||||
|
.join(exclude_glob.as_str())
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
for excluded_root in glob(&absolute_glob)
|
||||||
|
.map_err(|err| WorkspaceError::Pattern(absolute_glob.to_string(), err))?
|
||||||
|
{
|
||||||
|
let excluded_root = excluded_root
|
||||||
|
.map_err(|err| WorkspaceError::Glob(absolute_glob.to_string(), err))?;
|
||||||
|
if excluded_root == project_path {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[cfg(unix)] // Avoid path escaping for the unit tests
|
||||||
|
mod tests {
|
||||||
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use insta::assert_json_snapshot;
|
||||||
|
|
||||||
|
use crate::workspace::ProjectWorkspace;
|
||||||
|
|
||||||
|
fn workspace_test(folder: impl AsRef<Path>) -> (ProjectWorkspace, String) {
|
||||||
|
let root_dir = env::current_dir()
|
||||||
|
.unwrap()
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.join("scripts")
|
||||||
|
.join("workspaces");
|
||||||
|
let project = ProjectWorkspace::discover(root_dir.join(folder)).unwrap();
|
||||||
|
let root_escaped = regex::escape(root_dir.to_string_lossy().as_ref());
|
||||||
|
(project, root_escaped)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn albatross_in_example() {
|
||||||
|
let (project, root_escaped) = workspace_test("albatross-in-example/examples/bird-feeder");
|
||||||
|
let filters = vec![(root_escaped.as_str(), "[ROOT]")];
|
||||||
|
insta::with_settings!({filters => filters}, {
|
||||||
|
assert_json_snapshot!(
|
||||||
|
project,
|
||||||
|
{
|
||||||
|
".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]"
|
||||||
|
},
|
||||||
|
@r###"
|
||||||
|
{
|
||||||
|
"project_root": "[ROOT]/albatross-in-example/examples/bird-feeder",
|
||||||
|
"project_name": "bird-feeder",
|
||||||
|
"workspace": {
|
||||||
|
"root": "[ROOT]/albatross-in-example/examples/bird-feeder",
|
||||||
|
"packages": {
|
||||||
|
"bird-feeder": {
|
||||||
|
"root": "[ROOT]/albatross-in-example/examples/bird-feeder",
|
||||||
|
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sources": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn albatross_project_in_excluded() {
|
||||||
|
let (project, root_escaped) =
|
||||||
|
workspace_test("albatross-project-in-excluded/excluded/bird-feeder");
|
||||||
|
let filters = vec![(root_escaped.as_str(), "[ROOT]")];
|
||||||
|
insta::with_settings!({filters => filters}, {
|
||||||
|
assert_json_snapshot!(
|
||||||
|
project,
|
||||||
|
{
|
||||||
|
".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]"
|
||||||
|
},
|
||||||
|
@r###"
|
||||||
|
{
|
||||||
|
"project_root": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder",
|
||||||
|
"project_name": "bird-feeder",
|
||||||
|
"workspace": {
|
||||||
|
"root": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder",
|
||||||
|
"packages": {
|
||||||
|
"bird-feeder": {
|
||||||
|
"root": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder",
|
||||||
|
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sources": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn albatross_root_workspace() {
|
||||||
|
let (project, root_escaped) = workspace_test("albatross-root-workspace");
|
||||||
|
let filters = vec![(root_escaped.as_str(), "[ROOT]")];
|
||||||
|
insta::with_settings!({filters => filters}, {
|
||||||
|
assert_json_snapshot!(
|
||||||
|
project,
|
||||||
|
{
|
||||||
|
".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]"
|
||||||
|
},
|
||||||
|
@r###"
|
||||||
|
{
|
||||||
|
"project_root": "[ROOT]/albatross-root-workspace",
|
||||||
|
"project_name": "albatross",
|
||||||
|
"workspace": {
|
||||||
|
"root": "[ROOT]/albatross-root-workspace",
|
||||||
|
"packages": {
|
||||||
|
"albatross": {
|
||||||
|
"root": "[ROOT]/albatross-root-workspace",
|
||||||
|
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||||
|
},
|
||||||
|
"bird-feeder": {
|
||||||
|
"root": "[ROOT]/albatross-root-workspace/packages/bird-feeder",
|
||||||
|
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||||
|
},
|
||||||
|
"seeds": {
|
||||||
|
"root": "[ROOT]/albatross-root-workspace/packages/seeds",
|
||||||
|
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"bird-feeder": {
|
||||||
|
"workspace": true,
|
||||||
|
"editable": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn albatross_virtual_workspace() {
|
||||||
|
let (project, root_escaped) =
|
||||||
|
workspace_test("albatross-virtual-workspace/packages/albatross");
|
||||||
|
let filters = vec![(root_escaped.as_str(), "[ROOT]")];
|
||||||
|
insta::with_settings!({filters => filters}, {
|
||||||
|
assert_json_snapshot!(
|
||||||
|
project,
|
||||||
|
{
|
||||||
|
".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]"
|
||||||
|
},
|
||||||
|
@r###"
|
||||||
|
{
|
||||||
|
"project_root": "[ROOT]/albatross-virtual-workspace/packages/albatross",
|
||||||
|
"project_name": "albatross",
|
||||||
|
"workspace": {
|
||||||
|
"root": "[ROOT]/albatross-virtual-workspace",
|
||||||
|
"packages": {
|
||||||
|
"albatross": {
|
||||||
|
"root": "[ROOT]/albatross-virtual-workspace/packages/albatross",
|
||||||
|
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||||
|
},
|
||||||
|
"bird-feeder": {
|
||||||
|
"root": "[ROOT]/albatross-virtual-workspace/packages/bird-feeder",
|
||||||
|
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||||
|
},
|
||||||
|
"seeds": {
|
||||||
|
"root": "[ROOT]/albatross-virtual-workspace/packages/seeds",
|
||||||
|
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sources": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn albatross_just_project() {
|
||||||
|
let (project, root_escaped) = workspace_test("albatross-just-project");
|
||||||
|
let filters = vec![(root_escaped.as_str(), "[ROOT]")];
|
||||||
|
insta::with_settings!({filters => filters}, {
|
||||||
|
assert_json_snapshot!(
|
||||||
|
project,
|
||||||
|
{
|
||||||
|
".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]"
|
||||||
|
},
|
||||||
|
@r###"
|
||||||
|
{
|
||||||
|
"project_root": "[ROOT]/albatross-just-project",
|
||||||
|
"project_name": "albatross",
|
||||||
|
"workspace": {
|
||||||
|
"root": "[ROOT]/albatross-just-project",
|
||||||
|
"packages": {
|
||||||
|
"albatross": {
|
||||||
|
"root": "[ROOT]/albatross-just-project",
|
||||||
|
"pyproject_toml": "[PYPROJECT_TOML]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sources": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,99 +0,0 @@
|
||||||
use serde::Deserialize;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use tracing::debug;
|
|
||||||
use uv_fs::Simplified;
|
|
||||||
use uv_normalize::PackageName;
|
|
||||||
|
|
||||||
use uv_requirements::RequirementsSource;
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub(crate) enum ProjectError {
|
|
||||||
#[error(transparent)]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Toml(#[from] toml::de::Error),
|
|
||||||
|
|
||||||
#[error("No `project` section found in: {}", _0.user_display())]
|
|
||||||
MissingProject(PathBuf),
|
|
||||||
|
|
||||||
#[error("No `name` found in `project` section in: {}", _0.user_display())]
|
|
||||||
MissingName(PathBuf),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub(crate) struct Project {
|
|
||||||
/// The name of the package.
|
|
||||||
name: PackageName,
|
|
||||||
/// The path to the `pyproject.toml` file.
|
|
||||||
path: PathBuf,
|
|
||||||
/// The path to the project root.
|
|
||||||
root: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Project {
|
|
||||||
/// Find the current project.
|
|
||||||
pub(crate) fn find(path: impl AsRef<Path>) -> Result<Option<Self>, ProjectError> {
|
|
||||||
for ancestor in path.as_ref().ancestors() {
|
|
||||||
let pyproject_path = ancestor.join("pyproject.toml");
|
|
||||||
if pyproject_path.exists() {
|
|
||||||
debug!(
|
|
||||||
"Loading requirements from: {}",
|
|
||||||
pyproject_path.user_display()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Read the `pyproject.toml`.
|
|
||||||
let contents = fs_err::read_to_string(&pyproject_path)?;
|
|
||||||
let pyproject_toml: PyProjectToml = toml::from_str(&contents)?;
|
|
||||||
|
|
||||||
// Extract the package name.
|
|
||||||
let Some(project) = pyproject_toml.project else {
|
|
||||||
return Err(ProjectError::MissingProject(pyproject_path));
|
|
||||||
};
|
|
||||||
let Some(name) = project.name else {
|
|
||||||
return Err(ProjectError::MissingName(pyproject_path));
|
|
||||||
};
|
|
||||||
|
|
||||||
return Ok(Some(Self {
|
|
||||||
name,
|
|
||||||
path: pyproject_path,
|
|
||||||
root: ancestor.to_path_buf(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the [`PackageName`] for the project.
|
|
||||||
pub(crate) fn name(&self) -> &PackageName {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the root path for the project.
|
|
||||||
pub(crate) fn root(&self) -> &Path {
|
|
||||||
&self.root
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the requirements for the project.
|
|
||||||
pub(crate) fn requirements(&self) -> Vec<RequirementsSource> {
|
|
||||||
vec![
|
|
||||||
RequirementsSource::from_requirements_file(self.path.clone()),
|
|
||||||
RequirementsSource::from_source_tree(self.root.clone()),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A pyproject.toml as specified in PEP 517.
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
struct PyProjectToml {
|
|
||||||
project: Option<PyProjectProject>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
struct PyProjectProject {
|
|
||||||
name: Option<PackageName>,
|
|
||||||
}
|
|
|
@ -9,12 +9,11 @@ use uv_configuration::{
|
||||||
Concurrency, ConfigSettings, NoBinary, NoBuild, PreviewMode, Reinstall, SetupPyStrategy,
|
Concurrency, ConfigSettings, NoBinary, NoBuild, PreviewMode, Reinstall, SetupPyStrategy,
|
||||||
};
|
};
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
use uv_requirements::{ExtrasSpecification, RequirementsSpecification};
|
use uv_requirements::{ExtrasSpecification, ProjectWorkspace, RequirementsSpecification};
|
||||||
use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder};
|
use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder};
|
||||||
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
|
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
use crate::commands::project::discovery::Project;
|
|
||||||
use crate::commands::project::Error;
|
use crate::commands::project::Error;
|
||||||
use crate::commands::{project, ExitStatus};
|
use crate::commands::{project, ExitStatus};
|
||||||
use crate::editables::ResolvedEditables;
|
use crate::editables::ResolvedEditables;
|
||||||
|
@ -32,11 +31,7 @@ pub(crate) async fn lock(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the project requirements.
|
// Find the project requirements.
|
||||||
let Some(project) = Project::find(std::env::current_dir()?)? else {
|
let project = ProjectWorkspace::discover(std::env::current_dir()?)?;
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Unable to find `pyproject.toml` for project."
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Discover or create the virtual environment.
|
// Discover or create the virtual environment.
|
||||||
let venv = project::init(&project, cache, printer)?;
|
let venv = project::init(&project, cache, printer)?;
|
||||||
|
@ -111,7 +106,8 @@ pub(crate) async fn lock(
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Build all editable distributions. The editables are shared between resolution and
|
// Build all editable distributions. The editables are shared between resolution and
|
||||||
// installation, and should live for the duration of the command.
|
// installation, and should live for the duration of the command. If an editable is already
|
||||||
|
// installed in the environment, we'll still re-build it here.
|
||||||
let editables = ResolvedEditables::resolve(
|
let editables = ResolvedEditables::resolve(
|
||||||
spec.editables.clone(),
|
spec.editables.clone(),
|
||||||
&EmptyInstalledPackages,
|
&EmptyInstalledPackages,
|
||||||
|
@ -159,7 +155,11 @@ pub(crate) async fn lock(
|
||||||
// Write the lockfile to disk.
|
// Write the lockfile to disk.
|
||||||
let lock = resolution.lock()?;
|
let lock = resolution.lock()?;
|
||||||
let encoded = toml::to_string_pretty(&lock)?;
|
let encoded = toml::to_string_pretty(&lock)?;
|
||||||
fs_err::tokio::write(project.root().join("uv.lock"), encoded.as_bytes()).await?;
|
fs_err::tokio::write(
|
||||||
|
project.workspace().root().join("uv.lock"),
|
||||||
|
encoded.as_bytes(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(ExitStatus::Success)
|
Ok(ExitStatus::Success)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,8 @@ use uv_fs::Simplified;
|
||||||
use uv_installer::{Downloader, Plan, Planner, SatisfiesResult, SitePackages};
|
use uv_installer::{Downloader, Plan, Planner, SatisfiesResult, SitePackages};
|
||||||
use uv_interpreter::{find_default_python, Interpreter, PythonEnvironment};
|
use uv_interpreter::{find_default_python, Interpreter, PythonEnvironment};
|
||||||
use uv_requirements::{
|
use uv_requirements::{
|
||||||
ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSource,
|
ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, ProjectWorkspace,
|
||||||
RequirementsSpecification, SourceTreeResolver,
|
RequirementsSource, RequirementsSpecification, SourceTreeResolver,
|
||||||
};
|
};
|
||||||
use uv_resolver::{
|
use uv_resolver::{
|
||||||
Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, OptionsBuilder, PythonRequirement,
|
Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, OptionsBuilder, PythonRequirement,
|
||||||
|
@ -31,13 +31,11 @@ use uv_resolver::{
|
||||||
};
|
};
|
||||||
use uv_types::{BuildIsolation, HashStrategy, InFlight, InstalledPackagesProvider};
|
use uv_types::{BuildIsolation, HashStrategy, InFlight, InstalledPackagesProvider};
|
||||||
|
|
||||||
use crate::commands::project::discovery::Project;
|
|
||||||
use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
|
use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
|
||||||
use crate::commands::{elapsed, ChangeEvent, ChangeEventKind};
|
use crate::commands::{elapsed, ChangeEvent, ChangeEventKind};
|
||||||
use crate::editables::ResolvedEditables;
|
use crate::editables::ResolvedEditables;
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
|
||||||
mod discovery;
|
|
||||||
pub(crate) mod lock;
|
pub(crate) mod lock;
|
||||||
pub(crate) mod run;
|
pub(crate) mod run;
|
||||||
pub(crate) mod sync;
|
pub(crate) mod sync;
|
||||||
|
@ -80,11 +78,11 @@ pub(crate) enum Error {
|
||||||
|
|
||||||
/// Initialize a virtual environment for the current project.
|
/// Initialize a virtual environment for the current project.
|
||||||
pub(crate) fn init(
|
pub(crate) fn init(
|
||||||
project: &Project,
|
project: &ProjectWorkspace,
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
) -> Result<PythonEnvironment, Error> {
|
) -> Result<PythonEnvironment, Error> {
|
||||||
let venv = project.root().join(".venv");
|
let venv = project.workspace().root().join(".venv");
|
||||||
|
|
||||||
// Discover or create the virtual environment.
|
// Discover or create the virtual environment.
|
||||||
// TODO(charlie): If the environment isn't compatible with `--python`, recreate it.
|
// TODO(charlie): If the environment isn't compatible with `--python`, recreate it.
|
||||||
|
@ -165,7 +163,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
||||||
let requirements = {
|
let requirements = {
|
||||||
// Convert from unnamed to named requirements.
|
// Convert from unnamed to named requirements.
|
||||||
let mut requirements = NamedRequirementsResolver::new(
|
let mut requirements = NamedRequirementsResolver::new(
|
||||||
requirements,
|
requirements.clone(),
|
||||||
hasher,
|
hasher,
|
||||||
index,
|
index,
|
||||||
DistributionDatabase::new(client, build_dispatch, concurrency.downloads),
|
DistributionDatabase::new(client, build_dispatch, concurrency.downloads),
|
||||||
|
@ -178,7 +176,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
||||||
if !source_trees.is_empty() {
|
if !source_trees.is_empty() {
|
||||||
requirements.extend(
|
requirements.extend(
|
||||||
SourceTreeResolver::new(
|
SourceTreeResolver::new(
|
||||||
source_trees,
|
source_trees.clone(),
|
||||||
&ExtrasSpecification::None,
|
&ExtrasSpecification::None,
|
||||||
hasher,
|
hasher,
|
||||||
index,
|
index,
|
||||||
|
@ -214,7 +212,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
||||||
constraints,
|
constraints,
|
||||||
overrides,
|
overrides,
|
||||||
preferences,
|
preferences,
|
||||||
project,
|
project.clone(),
|
||||||
editable_metadata,
|
editable_metadata,
|
||||||
exclusions,
|
exclusions,
|
||||||
lookaheads,
|
lookaheads,
|
||||||
|
|
|
@ -10,10 +10,9 @@ use tracing::debug;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_configuration::PreviewMode;
|
use uv_configuration::PreviewMode;
|
||||||
use uv_interpreter::PythonEnvironment;
|
use uv_interpreter::PythonEnvironment;
|
||||||
use uv_requirements::RequirementsSource;
|
use uv_requirements::{ProjectWorkspace, RequirementsSource};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
use crate::commands::project::discovery::Project;
|
|
||||||
use crate::commands::{project, ExitStatus};
|
use crate::commands::{project, ExitStatus};
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
|
||||||
|
@ -55,11 +54,7 @@ pub(crate) async fn run(
|
||||||
} else {
|
} else {
|
||||||
debug!("Syncing project environment.");
|
debug!("Syncing project environment.");
|
||||||
|
|
||||||
let Some(project) = Project::find(std::env::current_dir()?)? else {
|
let project = ProjectWorkspace::discover(std::env::current_dir()?)?;
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Unable to find `pyproject.toml` for project."
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
let venv = project::init(&project, cache, printer)?;
|
let venv = project::init(&project, cache, printer)?;
|
||||||
|
|
||||||
|
|
|
@ -5,15 +5,15 @@ use install_wheel_rs::linker::LinkMode;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::RegistryClientBuilder;
|
use uv_client::RegistryClientBuilder;
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
Concurrency, ConfigSettings, NoBinary, NoBuild, PreviewMode, SetupPyStrategy,
|
Concurrency, ConfigSettings, NoBinary, NoBuild, PreviewMode, Reinstall, SetupPyStrategy,
|
||||||
};
|
};
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
use uv_installer::SitePackages;
|
use uv_installer::SitePackages;
|
||||||
|
use uv_requirements::ProjectWorkspace;
|
||||||
use uv_resolver::{FlatIndex, InMemoryIndex, Lock};
|
use uv_resolver::{FlatIndex, InMemoryIndex, Lock};
|
||||||
use uv_types::{BuildIsolation, HashStrategy, InFlight};
|
use uv_types::{BuildIsolation, HashStrategy, InFlight};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
use crate::commands::project::discovery::Project;
|
|
||||||
use crate::commands::{project, ExitStatus};
|
use crate::commands::{project, ExitStatus};
|
||||||
use crate::editables::ResolvedEditables;
|
use crate::editables::ResolvedEditables;
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
@ -30,11 +30,7 @@ pub(crate) async fn sync(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the project requirements.
|
// Find the project requirements.
|
||||||
let Some(project) = Project::find(std::env::current_dir()?)? else {
|
let project = ProjectWorkspace::discover(std::env::current_dir()?)?;
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Unable to find `pyproject.toml` for project."
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Discover or create the virtual environment.
|
// Discover or create the virtual environment.
|
||||||
let venv = project::init(&project, cache, printer)?;
|
let venv = project::init(&project, cache, printer)?;
|
||||||
|
@ -43,9 +39,10 @@ pub(crate) async fn sync(
|
||||||
|
|
||||||
// Read the lockfile.
|
// Read the lockfile.
|
||||||
let resolution = {
|
let resolution = {
|
||||||
let encoded = fs_err::tokio::read_to_string(project.root().join("uv.lock")).await?;
|
let encoded =
|
||||||
|
fs_err::tokio::read_to_string(project.workspace().root().join("uv.lock")).await?;
|
||||||
let lock: Lock = toml::from_str(&encoded)?;
|
let lock: Lock = toml::from_str(&encoded)?;
|
||||||
lock.to_resolution(markers, tags, project.name())
|
lock.to_resolution(markers, tags, project.project_name())
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize the registry client.
|
// Initialize the registry client.
|
||||||
|
@ -55,6 +52,8 @@ pub(crate) async fn sync(
|
||||||
.platform(venv.interpreter().platform())
|
.platform(venv.interpreter().platform())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
let site_packages = SitePackages::from_executable(&venv)?;
|
||||||
|
|
||||||
// TODO(charlie): Respect project configuration.
|
// TODO(charlie): Respect project configuration.
|
||||||
let build_isolation = BuildIsolation::default();
|
let build_isolation = BuildIsolation::default();
|
||||||
let config_settings = ConfigSettings::default();
|
let config_settings = ConfigSettings::default();
|
||||||
|
@ -68,6 +67,7 @@ pub(crate) async fn sync(
|
||||||
let no_build = NoBuild::default();
|
let no_build = NoBuild::default();
|
||||||
let setup_py = SetupPyStrategy::default();
|
let setup_py = SetupPyStrategy::default();
|
||||||
let concurrency = Concurrency::default();
|
let concurrency = Concurrency::default();
|
||||||
|
let reinstall = Reinstall::None;
|
||||||
|
|
||||||
// Create a build dispatch.
|
// Create a build dispatch.
|
||||||
let build_dispatch = BuildDispatch::new(
|
let build_dispatch = BuildDispatch::new(
|
||||||
|
@ -87,8 +87,20 @@ pub(crate) async fn sync(
|
||||||
concurrency,
|
concurrency,
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO(konsti): Read editables from lockfile.
|
let editables = ResolvedEditables::resolve(
|
||||||
let editables = ResolvedEditables::default();
|
Vec::new(), // TODO(konsti): Read editables from lockfile
|
||||||
|
&site_packages,
|
||||||
|
&reinstall,
|
||||||
|
&hasher,
|
||||||
|
venv.interpreter(),
|
||||||
|
tags,
|
||||||
|
cache,
|
||||||
|
&client,
|
||||||
|
&build_dispatch,
|
||||||
|
concurrency,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let site_packages = SitePackages::from_executable(&venv)?;
|
let site_packages = SitePackages::from_executable(&venv)?;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
exclude = [
|
exclude = [
|
||||||
"crates/uv-virtualenv/src/activator/activate_this.py",
|
"crates/uv-virtualenv/src/activator/activate_this.py",
|
||||||
"crates/uv-virtualenv/src/_virtualenv.py"
|
"crates/uv-virtualenv/src/_virtualenv.py",
|
||||||
|
"scripts/workspaces"
|
||||||
]
|
]
|
||||||
[lint.per-file-ignores]
|
[lint.per-file-ignores]
|
||||||
"__init__.py" = ["F403", "F405"]
|
"__init__.py" = ["F403", "F405"]
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
from albatross import fly
|
||||||
|
|
||||||
|
try:
|
||||||
|
from bird_feeder import use
|
||||||
|
|
||||||
|
raise RuntimeError("bird-feeder installed")
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
fly()
|
||||||
|
print("Success")
|
|
@ -0,0 +1,10 @@
|
||||||
|
from bird_feeder import use
|
||||||
|
|
||||||
|
try:
|
||||||
|
from albatross import fly
|
||||||
|
|
||||||
|
raise RuntimeError("albatross installed")
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("Success")
|
|
@ -0,0 +1,8 @@
|
||||||
|
[project]
|
||||||
|
name = "bird-feeder"
|
||||||
|
version = "1.0.0"
|
||||||
|
dependencies = ["anyio>=4.3.0,<5"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
|
@ -0,0 +1,5 @@
|
||||||
|
import anyio
|
||||||
|
|
||||||
|
|
||||||
|
def use():
|
||||||
|
print("squirrel")
|
8
scripts/workspaces/albatross-in-example/pyproject.toml
Normal file
8
scripts/workspaces/albatross-in-example/pyproject.toml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
[project]
|
||||||
|
name = "albatross"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = ["tqdm>=4,<5"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
|
@ -0,0 +1,9 @@
|
||||||
|
import tqdm
|
||||||
|
|
||||||
|
|
||||||
|
def fly():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("Caw")
|
|
@ -0,0 +1,4 @@
|
||||||
|
from albatross import fly
|
||||||
|
|
||||||
|
fly()
|
||||||
|
print("Success")
|
8
scripts/workspaces/albatross-just-project/pyproject.toml
Normal file
8
scripts/workspaces/albatross-just-project/pyproject.toml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
[project]
|
||||||
|
name = "albatross"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = ["tqdm>=4,<5"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
|
@ -0,0 +1,2 @@
|
||||||
|
def fly():
|
||||||
|
pass
|
|
@ -0,0 +1,10 @@
|
||||||
|
from bird_feeder import use
|
||||||
|
|
||||||
|
try:
|
||||||
|
from albatross import fly
|
||||||
|
|
||||||
|
raise RuntimeError("albatross installed")
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("Success")
|
|
@ -0,0 +1,8 @@
|
||||||
|
[project]
|
||||||
|
name = "bird-feeder"
|
||||||
|
version = "1.0.0"
|
||||||
|
dependencies = ["anyio>=4.3.0,<5"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
|
@ -0,0 +1,5 @@
|
||||||
|
import anyio
|
||||||
|
|
||||||
|
|
||||||
|
def use():
|
||||||
|
print("squirrel")
|
|
@ -0,0 +1,12 @@
|
||||||
|
[project]
|
||||||
|
name = "albatross"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = ["tqdm>=4,<5"]
|
||||||
|
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = ["packages/*"]
|
||||||
|
exclude = ["excluded/*"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
|
@ -0,0 +1,5 @@
|
||||||
|
import tqdm
|
||||||
|
from bird_feeder import use
|
||||||
|
|
||||||
|
print("Caw")
|
||||||
|
use()
|
|
@ -0,0 +1,4 @@
|
||||||
|
from albatross import fly
|
||||||
|
|
||||||
|
fly()
|
||||||
|
print("Success")
|
|
@ -0,0 +1,10 @@
|
||||||
|
from bird_feeder import use
|
||||||
|
|
||||||
|
try:
|
||||||
|
from albatross import fly
|
||||||
|
|
||||||
|
raise RuntimeError("albatross installed")
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("Success")
|
|
@ -0,0 +1,11 @@
|
||||||
|
[project]
|
||||||
|
name = "bird-feeder"
|
||||||
|
version = "1.0.0"
|
||||||
|
dependencies = ["anyio>=4.3.0,<5", "seeds"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
seeds = { workspace = true }
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
|
@ -0,0 +1,5 @@
|
||||||
|
import anyio
|
||||||
|
|
||||||
|
|
||||||
|
def use():
|
||||||
|
print("squirrel")
|
|
@ -0,0 +1,8 @@
|
||||||
|
[project]
|
||||||
|
name = "seeds"
|
||||||
|
version = "1.0.0"
|
||||||
|
dependencies = ["boltons==24.0.0"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
|
@ -0,0 +1,5 @@
|
||||||
|
import boltons
|
||||||
|
|
||||||
|
|
||||||
|
def seeds():
|
||||||
|
print("sunflower")
|
14
scripts/workspaces/albatross-root-workspace/pyproject.toml
Normal file
14
scripts/workspaces/albatross-root-workspace/pyproject.toml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[project]
|
||||||
|
name = "albatross"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = ["bird-feeder", "tqdm>=4,<5"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
bird-feeder = { workspace = true }
|
||||||
|
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = ["packages/*"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
|
@ -0,0 +1,11 @@
|
||||||
|
import tqdm
|
||||||
|
from bird_feeder import use
|
||||||
|
|
||||||
|
|
||||||
|
def fly():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("Caw")
|
||||||
|
use()
|
|
@ -0,0 +1,4 @@
|
||||||
|
from albatross import fly
|
||||||
|
|
||||||
|
fly()
|
||||||
|
print("Success")
|
|
@ -0,0 +1,11 @@
|
||||||
|
[project]
|
||||||
|
name = "albatross"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = ["bird-feeder", "tqdm>=4,<5"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
bird-feeder = { workspace = true }
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
|
@ -0,0 +1,11 @@
|
||||||
|
import tqdm
|
||||||
|
from bird_feeder import use
|
||||||
|
|
||||||
|
|
||||||
|
def fly():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("Caw")
|
||||||
|
use()
|
|
@ -0,0 +1,10 @@
|
||||||
|
from bird_feeder import use
|
||||||
|
|
||||||
|
try:
|
||||||
|
from albatross import fly
|
||||||
|
|
||||||
|
raise RuntimeError("albatross installed")
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("Success")
|
|
@ -0,0 +1,11 @@
|
||||||
|
[project]
|
||||||
|
name = "bird-feeder"
|
||||||
|
version = "1.0.0"
|
||||||
|
dependencies = ["anyio>=4.3.0,<5", "seeds"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
seeds = { workspace = true }
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
|
@ -0,0 +1,5 @@
|
||||||
|
import anyio
|
||||||
|
|
||||||
|
|
||||||
|
def use():
|
||||||
|
print("squirrel")
|
|
@ -0,0 +1,8 @@
|
||||||
|
[project]
|
||||||
|
name = "seeds"
|
||||||
|
version = "1.0.0"
|
||||||
|
dependencies = ["boltons==24.0.0"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
|
@ -0,0 +1,5 @@
|
||||||
|
import boltons
|
||||||
|
|
||||||
|
|
||||||
|
def seeds():
|
||||||
|
print("sunflower")
|
|
@ -0,0 +1,2 @@
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = ["packages/*"]
|
Loading…
Add table
Add a link
Reference in a new issue