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",
|
||||
"pep508_rs",
|
||||
"pypi-types",
|
||||
"regex",
|
||||
"requirements-txt",
|
||||
"rustc-hash",
|
||||
"schemars",
|
||||
|
|
|
@ -50,7 +50,8 @@ schemars = ["dep:schemars"]
|
|||
|
||||
[dev-dependencies]
|
||||
indoc = "2.0.5"
|
||||
insta = "1.38.0"
|
||||
insta = { version = "1.38.0", features = ["filters", "redactions", "json"] }
|
||||
regex = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
@ -3,6 +3,7 @@ pub use crate::source_tree::*;
|
|||
pub use crate::sources::*;
|
||||
pub use crate::specification::*;
|
||||
pub use crate::unnamed::*;
|
||||
pub use crate::workspace::*;
|
||||
|
||||
mod confirm;
|
||||
mod lookahead;
|
||||
|
@ -12,3 +13,4 @@ mod sources;
|
|||
mod specification;
|
||||
mod unnamed;
|
||||
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}`
|
||||
//! * `tool.uv.sources`
|
||||
|
@ -6,7 +6,7 @@
|
|||
//!
|
||||
//! Then lowers them into a dependency specification.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
use std::io;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
@ -110,7 +110,7 @@ pub struct Tool {
|
|||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct ToolUv {
|
||||
pub sources: Option<HashMap<PackageName, Source>>,
|
||||
pub sources: Option<BTreeMap<PackageName, Source>>,
|
||||
pub workspace: Option<ToolUvWorkspace>,
|
||||
}
|
||||
|
||||
|
@ -238,8 +238,8 @@ impl Pep621Metadata {
|
|||
extras: &ExtrasSpecification,
|
||||
pyproject_path: &Path,
|
||||
project_dir: &Path,
|
||||
workspace_sources: &HashMap<PackageName, Source>,
|
||||
workspace_packages: &HashMap<PackageName, String>,
|
||||
workspace_sources: &BTreeMap<PackageName, Source>,
|
||||
workspace_packages: &BTreeMap<PackageName, String>,
|
||||
preview: PreviewMode,
|
||||
) -> Result<Option<Self>, Pep621Error> {
|
||||
let project_sources = pyproject
|
||||
|
@ -323,9 +323,9 @@ pub(crate) fn lower_requirements(
|
|||
pyproject_path: &Path,
|
||||
project_name: &PackageName,
|
||||
project_dir: &Path,
|
||||
project_sources: &HashMap<PackageName, Source>,
|
||||
workspace_sources: &HashMap<PackageName, Source>,
|
||||
workspace_packages: &HashMap<PackageName, String>,
|
||||
project_sources: &BTreeMap<PackageName, Source>,
|
||||
workspace_sources: &BTreeMap<PackageName, Source>,
|
||||
workspace_packages: &BTreeMap<PackageName, String>,
|
||||
preview: PreviewMode,
|
||||
) -> Result<Requirements, Pep621Error> {
|
||||
let dependencies = dependencies
|
||||
|
@ -386,9 +386,9 @@ pub(crate) fn lower_requirement(
|
|||
requirement: pep508_rs::Requirement,
|
||||
project_name: &PackageName,
|
||||
project_dir: &Path,
|
||||
project_sources: &HashMap<PackageName, Source>,
|
||||
workspace_sources: &HashMap<PackageName, Source>,
|
||||
workspace_packages: &HashMap<PackageName, String>,
|
||||
project_sources: &BTreeMap<PackageName, Source>,
|
||||
workspace_sources: &BTreeMap<PackageName, Source>,
|
||||
workspace_packages: &BTreeMap<PackageName, String>,
|
||||
preview: PreviewMode,
|
||||
) -> Result<Requirement, LoweringError> {
|
||||
let source = project_sources
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
@ -165,8 +165,8 @@ impl RequirementsSpecification {
|
|||
.parent()
|
||||
.context("`pyproject.toml` has no parent directory")?;
|
||||
|
||||
let workspace_sources = HashMap::default();
|
||||
let workspace_packages = HashMap::default();
|
||||
let workspace_sources = BTreeMap::default();
|
||||
let workspace_packages = BTreeMap::default();
|
||||
match Pep621Metadata::try_from(
|
||||
pyproject,
|
||||
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,
|
||||
};
|
||||
use uv_dispatch::BuildDispatch;
|
||||
use uv_requirements::{ExtrasSpecification, RequirementsSpecification};
|
||||
use uv_requirements::{ExtrasSpecification, ProjectWorkspace, RequirementsSpecification};
|
||||
use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder};
|
||||
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
use crate::commands::project::discovery::Project;
|
||||
use crate::commands::project::Error;
|
||||
use crate::commands::{project, ExitStatus};
|
||||
use crate::editables::ResolvedEditables;
|
||||
|
@ -32,11 +31,7 @@ pub(crate) async fn lock(
|
|||
}
|
||||
|
||||
// Find the project requirements.
|
||||
let Some(project) = Project::find(std::env::current_dir()?)? else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Unable to find `pyproject.toml` for project."
|
||||
));
|
||||
};
|
||||
let project = ProjectWorkspace::discover(std::env::current_dir()?)?;
|
||||
|
||||
// Discover or create the virtual environment.
|
||||
let venv = project::init(&project, cache, printer)?;
|
||||
|
@ -111,7 +106,8 @@ pub(crate) async fn lock(
|
|||
.build();
|
||||
|
||||
// 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(
|
||||
spec.editables.clone(),
|
||||
&EmptyInstalledPackages,
|
||||
|
@ -159,7 +155,11 @@ pub(crate) async fn lock(
|
|||
// Write the lockfile to disk.
|
||||
let lock = resolution.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)
|
||||
}
|
||||
|
|
|
@ -22,8 +22,8 @@ use uv_fs::Simplified;
|
|||
use uv_installer::{Downloader, Plan, Planner, SatisfiesResult, SitePackages};
|
||||
use uv_interpreter::{find_default_python, Interpreter, PythonEnvironment};
|
||||
use uv_requirements::{
|
||||
ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSource,
|
||||
RequirementsSpecification, SourceTreeResolver,
|
||||
ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, ProjectWorkspace,
|
||||
RequirementsSource, RequirementsSpecification, SourceTreeResolver,
|
||||
};
|
||||
use uv_resolver::{
|
||||
Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, OptionsBuilder, PythonRequirement,
|
||||
|
@ -31,13 +31,11 @@ use uv_resolver::{
|
|||
};
|
||||
use uv_types::{BuildIsolation, HashStrategy, InFlight, InstalledPackagesProvider};
|
||||
|
||||
use crate::commands::project::discovery::Project;
|
||||
use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
|
||||
use crate::commands::{elapsed, ChangeEvent, ChangeEventKind};
|
||||
use crate::editables::ResolvedEditables;
|
||||
use crate::printer::Printer;
|
||||
|
||||
mod discovery;
|
||||
pub(crate) mod lock;
|
||||
pub(crate) mod run;
|
||||
pub(crate) mod sync;
|
||||
|
@ -80,11 +78,11 @@ pub(crate) enum Error {
|
|||
|
||||
/// Initialize a virtual environment for the current project.
|
||||
pub(crate) fn init(
|
||||
project: &Project,
|
||||
project: &ProjectWorkspace,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> Result<PythonEnvironment, Error> {
|
||||
let venv = project.root().join(".venv");
|
||||
let venv = project.workspace().root().join(".venv");
|
||||
|
||||
// Discover or create the virtual environment.
|
||||
// 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 = {
|
||||
// Convert from unnamed to named requirements.
|
||||
let mut requirements = NamedRequirementsResolver::new(
|
||||
requirements,
|
||||
requirements.clone(),
|
||||
hasher,
|
||||
index,
|
||||
DistributionDatabase::new(client, build_dispatch, concurrency.downloads),
|
||||
|
@ -178,7 +176,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
|||
if !source_trees.is_empty() {
|
||||
requirements.extend(
|
||||
SourceTreeResolver::new(
|
||||
source_trees,
|
||||
source_trees.clone(),
|
||||
&ExtrasSpecification::None,
|
||||
hasher,
|
||||
index,
|
||||
|
@ -214,7 +212,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
|||
constraints,
|
||||
overrides,
|
||||
preferences,
|
||||
project,
|
||||
project.clone(),
|
||||
editable_metadata,
|
||||
exclusions,
|
||||
lookaheads,
|
||||
|
|
|
@ -10,10 +10,9 @@ use tracing::debug;
|
|||
use uv_cache::Cache;
|
||||
use uv_configuration::PreviewMode;
|
||||
use uv_interpreter::PythonEnvironment;
|
||||
use uv_requirements::RequirementsSource;
|
||||
use uv_requirements::{ProjectWorkspace, RequirementsSource};
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
use crate::commands::project::discovery::Project;
|
||||
use crate::commands::{project, ExitStatus};
|
||||
use crate::printer::Printer;
|
||||
|
||||
|
@ -55,11 +54,7 @@ pub(crate) async fn run(
|
|||
} else {
|
||||
debug!("Syncing project environment.");
|
||||
|
||||
let Some(project) = Project::find(std::env::current_dir()?)? else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Unable to find `pyproject.toml` for project."
|
||||
));
|
||||
};
|
||||
let project = ProjectWorkspace::discover(std::env::current_dir()?)?;
|
||||
|
||||
let venv = project::init(&project, cache, printer)?;
|
||||
|
||||
|
|
|
@ -5,15 +5,15 @@ use install_wheel_rs::linker::LinkMode;
|
|||
use uv_cache::Cache;
|
||||
use uv_client::RegistryClientBuilder;
|
||||
use uv_configuration::{
|
||||
Concurrency, ConfigSettings, NoBinary, NoBuild, PreviewMode, SetupPyStrategy,
|
||||
Concurrency, ConfigSettings, NoBinary, NoBuild, PreviewMode, Reinstall, SetupPyStrategy,
|
||||
};
|
||||
use uv_dispatch::BuildDispatch;
|
||||
use uv_installer::SitePackages;
|
||||
use uv_requirements::ProjectWorkspace;
|
||||
use uv_resolver::{FlatIndex, InMemoryIndex, Lock};
|
||||
use uv_types::{BuildIsolation, HashStrategy, InFlight};
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
use crate::commands::project::discovery::Project;
|
||||
use crate::commands::{project, ExitStatus};
|
||||
use crate::editables::ResolvedEditables;
|
||||
use crate::printer::Printer;
|
||||
|
@ -30,11 +30,7 @@ pub(crate) async fn sync(
|
|||
}
|
||||
|
||||
// Find the project requirements.
|
||||
let Some(project) = Project::find(std::env::current_dir()?)? else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Unable to find `pyproject.toml` for project."
|
||||
));
|
||||
};
|
||||
let project = ProjectWorkspace::discover(std::env::current_dir()?)?;
|
||||
|
||||
// Discover or create the virtual environment.
|
||||
let venv = project::init(&project, cache, printer)?;
|
||||
|
@ -43,9 +39,10 @@ pub(crate) async fn sync(
|
|||
|
||||
// Read the lockfile.
|
||||
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)?;
|
||||
lock.to_resolution(markers, tags, project.name())
|
||||
lock.to_resolution(markers, tags, project.project_name())
|
||||
};
|
||||
|
||||
// Initialize the registry client.
|
||||
|
@ -55,6 +52,8 @@ pub(crate) async fn sync(
|
|||
.platform(venv.interpreter().platform())
|
||||
.build();
|
||||
|
||||
let site_packages = SitePackages::from_executable(&venv)?;
|
||||
|
||||
// TODO(charlie): Respect project configuration.
|
||||
let build_isolation = BuildIsolation::default();
|
||||
let config_settings = ConfigSettings::default();
|
||||
|
@ -68,6 +67,7 @@ pub(crate) async fn sync(
|
|||
let no_build = NoBuild::default();
|
||||
let setup_py = SetupPyStrategy::default();
|
||||
let concurrency = Concurrency::default();
|
||||
let reinstall = Reinstall::None;
|
||||
|
||||
// Create a build dispatch.
|
||||
let build_dispatch = BuildDispatch::new(
|
||||
|
@ -87,8 +87,20 @@ pub(crate) async fn sync(
|
|||
concurrency,
|
||||
);
|
||||
|
||||
// TODO(konsti): Read editables from lockfile.
|
||||
let editables = ResolvedEditables::default();
|
||||
let editables = ResolvedEditables::resolve(
|
||||
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)?;
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
exclude = [
|
||||
"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]
|
||||
"__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