diff --git a/Cargo.lock b/Cargo.lock index 7dad90cf952a..0224f9dde067 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4955,6 +4955,7 @@ dependencies = [ "pep440_rs", "pep508_rs", "pypi-types", + "regex", "requirements-txt", "rustc-hash", "schemars", diff --git a/crates/uv-requirements/Cargo.toml b/crates/uv-requirements/Cargo.toml index b307ba7cd9a6..ead65f8bf1cb 100644 --- a/crates/uv-requirements/Cargo.toml +++ b/crates/uv-requirements/Cargo.toml @@ -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 diff --git a/crates/uv-requirements/src/lib.rs b/crates/uv-requirements/src/lib.rs index a0b4101cab98..ec5c188ca4f4 100644 --- a/crates/uv-requirements/src/lib.rs +++ b/crates/uv-requirements/src/lib.rs @@ -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; diff --git a/crates/uv-requirements/src/pyproject.rs b/crates/uv-requirements/src/pyproject.rs index 873e5cc6e512..484d573a6be5 100644 --- a/crates/uv-requirements/src/pyproject.rs +++ b/crates/uv-requirements/src/pyproject.rs @@ -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>, + pub sources: Option>, pub workspace: Option, } @@ -238,8 +238,8 @@ impl Pep621Metadata { extras: &ExtrasSpecification, pyproject_path: &Path, project_dir: &Path, - workspace_sources: &HashMap, - workspace_packages: &HashMap, + workspace_sources: &BTreeMap, + workspace_packages: &BTreeMap, preview: PreviewMode, ) -> Result, 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, - workspace_sources: &HashMap, - workspace_packages: &HashMap, + project_sources: &BTreeMap, + workspace_sources: &BTreeMap, + workspace_packages: &BTreeMap, preview: PreviewMode, ) -> Result { 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, - workspace_sources: &HashMap, - workspace_packages: &HashMap, + project_sources: &BTreeMap, + workspace_sources: &BTreeMap, + workspace_packages: &BTreeMap, preview: PreviewMode, ) -> Result { let source = project_sources diff --git a/crates/uv-requirements/src/specification.rs b/crates/uv-requirements/src/specification.rs index 085166f0b4be..9b5291781aca 100644 --- a/crates/uv-requirements/src/specification.rs +++ b/crates/uv-requirements/src/specification.rs @@ -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, diff --git a/crates/uv-requirements/src/workspace.rs b/crates/uv-requirements/src/workspace.rs new file mode 100644 index 000000000000..3697f611066d --- /dev/null +++ b/crates/uv-requirements/src/workspace.rs @@ -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, + /// The sources table from the workspace `pyproject.toml`. It is overridden by the project + /// sources. + sources: BTreeMap, +} + +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 { + &self.packages + } + + /// The sources table from the workspace `pyproject.toml`. + pub fn sources(&self) -> &BTreeMap { + &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 `/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 `/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) -> Result { + 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 { + 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 { + 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 { + 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, 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 { + 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) -> (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": {} + } + } + "###); + }); + } +} diff --git a/crates/uv/src/commands/project/discovery.rs b/crates/uv/src/commands/project/discovery.rs deleted file mode 100644 index 77a5625ef271..000000000000 --- a/crates/uv/src/commands/project/discovery.rs +++ /dev/null @@ -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) -> Result, 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 { - 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, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "kebab-case")] -struct PyProjectProject { - name: Option, -} diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 14b196b85ffd..456f2792f4da 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -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) } diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index c173c35f3ca7..dd4476467188 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -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 { - 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( 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( 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( constraints, overrides, preferences, - project, + project.clone(), editable_metadata, exclusions, lookaheads, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 90d60080ba95..ae9f9f934419 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -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)?; diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index fc0871b6848b..607c87ec6f20 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -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)?; diff --git a/ruff.toml b/ruff.toml index 8c2f9e097c0d..d03315990279 100644 --- a/ruff.toml +++ b/ruff.toml @@ -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"] diff --git a/scripts/workspaces/albatross-in-example/check_installed_albatross.py b/scripts/workspaces/albatross-in-example/check_installed_albatross.py new file mode 100644 index 000000000000..3bae3c51b4a6 --- /dev/null +++ b/scripts/workspaces/albatross-in-example/check_installed_albatross.py @@ -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") diff --git a/scripts/workspaces/albatross-in-example/examples/bird-feeder/check_installed_bird_feeder.py b/scripts/workspaces/albatross-in-example/examples/bird-feeder/check_installed_bird_feeder.py new file mode 100644 index 000000000000..b1dc3e72facb --- /dev/null +++ b/scripts/workspaces/albatross-in-example/examples/bird-feeder/check_installed_bird_feeder.py @@ -0,0 +1,10 @@ +from bird_feeder import use + +try: + from albatross import fly + + raise RuntimeError("albatross installed") +except ModuleNotFoundError: + pass + +print("Success") diff --git a/scripts/workspaces/albatross-in-example/examples/bird-feeder/pyproject.toml b/scripts/workspaces/albatross-in-example/examples/bird-feeder/pyproject.toml new file mode 100644 index 000000000000..0bb570bb3fb9 --- /dev/null +++ b/scripts/workspaces/albatross-in-example/examples/bird-feeder/pyproject.toml @@ -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" diff --git a/scripts/workspaces/albatross-in-example/examples/bird-feeder/src/bird_feeder/__init__.py b/scripts/workspaces/albatross-in-example/examples/bird-feeder/src/bird_feeder/__init__.py new file mode 100644 index 000000000000..856404fc346d --- /dev/null +++ b/scripts/workspaces/albatross-in-example/examples/bird-feeder/src/bird_feeder/__init__.py @@ -0,0 +1,5 @@ +import anyio + + +def use(): + print("squirrel") diff --git a/scripts/workspaces/albatross-in-example/pyproject.toml b/scripts/workspaces/albatross-in-example/pyproject.toml new file mode 100644 index 000000000000..9a61c9de0c1a --- /dev/null +++ b/scripts/workspaces/albatross-in-example/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "albatross" +version = "0.1.0" +dependencies = ["tqdm>=4,<5"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/scripts/workspaces/albatross-in-example/src/albatross/__init__.py b/scripts/workspaces/albatross-in-example/src/albatross/__init__.py new file mode 100644 index 000000000000..02962aa5a7ef --- /dev/null +++ b/scripts/workspaces/albatross-in-example/src/albatross/__init__.py @@ -0,0 +1,9 @@ +import tqdm + + +def fly(): + pass + + +if __name__ == '__main__': + print("Caw") diff --git a/scripts/workspaces/albatross-just-project/check_installed_albatross.py b/scripts/workspaces/albatross-just-project/check_installed_albatross.py new file mode 100644 index 000000000000..e2e52c015f78 --- /dev/null +++ b/scripts/workspaces/albatross-just-project/check_installed_albatross.py @@ -0,0 +1,4 @@ +from albatross import fly + +fly() +print("Success") diff --git a/scripts/workspaces/albatross-just-project/pyproject.toml b/scripts/workspaces/albatross-just-project/pyproject.toml new file mode 100644 index 000000000000..9a61c9de0c1a --- /dev/null +++ b/scripts/workspaces/albatross-just-project/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "albatross" +version = "0.1.0" +dependencies = ["tqdm>=4,<5"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/scripts/workspaces/albatross-just-project/src/albatross/__init__.py b/scripts/workspaces/albatross-just-project/src/albatross/__init__.py new file mode 100644 index 000000000000..c36c6606c88c --- /dev/null +++ b/scripts/workspaces/albatross-just-project/src/albatross/__init__.py @@ -0,0 +1,2 @@ +def fly(): + pass diff --git a/scripts/workspaces/albatross-project-in-excluded/excluded/bird-feeder/check_installed_bird_feeder.py b/scripts/workspaces/albatross-project-in-excluded/excluded/bird-feeder/check_installed_bird_feeder.py new file mode 100644 index 000000000000..b1dc3e72facb --- /dev/null +++ b/scripts/workspaces/albatross-project-in-excluded/excluded/bird-feeder/check_installed_bird_feeder.py @@ -0,0 +1,10 @@ +from bird_feeder import use + +try: + from albatross import fly + + raise RuntimeError("albatross installed") +except ModuleNotFoundError: + pass + +print("Success") diff --git a/scripts/workspaces/albatross-project-in-excluded/excluded/bird-feeder/pyproject.toml b/scripts/workspaces/albatross-project-in-excluded/excluded/bird-feeder/pyproject.toml new file mode 100644 index 000000000000..0bb570bb3fb9 --- /dev/null +++ b/scripts/workspaces/albatross-project-in-excluded/excluded/bird-feeder/pyproject.toml @@ -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" diff --git a/scripts/workspaces/albatross-project-in-excluded/excluded/bird-feeder/src/bird_feeder/__init__.py b/scripts/workspaces/albatross-project-in-excluded/excluded/bird-feeder/src/bird_feeder/__init__.py new file mode 100644 index 000000000000..856404fc346d --- /dev/null +++ b/scripts/workspaces/albatross-project-in-excluded/excluded/bird-feeder/src/bird_feeder/__init__.py @@ -0,0 +1,5 @@ +import anyio + + +def use(): + print("squirrel") diff --git a/scripts/workspaces/albatross-project-in-excluded/pyproject.toml b/scripts/workspaces/albatross-project-in-excluded/pyproject.toml new file mode 100644 index 000000000000..de024d923c2f --- /dev/null +++ b/scripts/workspaces/albatross-project-in-excluded/pyproject.toml @@ -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" diff --git a/scripts/workspaces/albatross-project-in-excluded/src/albatross/__init__.py b/scripts/workspaces/albatross-project-in-excluded/src/albatross/__init__.py new file mode 100644 index 000000000000..99769087f1cb --- /dev/null +++ b/scripts/workspaces/albatross-project-in-excluded/src/albatross/__init__.py @@ -0,0 +1,5 @@ +import tqdm +from bird_feeder import use + +print("Caw") +use() diff --git a/scripts/workspaces/albatross-root-workspace/check_installed_albatross.py b/scripts/workspaces/albatross-root-workspace/check_installed_albatross.py new file mode 100644 index 000000000000..e2e52c015f78 --- /dev/null +++ b/scripts/workspaces/albatross-root-workspace/check_installed_albatross.py @@ -0,0 +1,4 @@ +from albatross import fly + +fly() +print("Success") diff --git a/scripts/workspaces/albatross-root-workspace/packages/bird-feeder/check_installed_bird_feeder.py b/scripts/workspaces/albatross-root-workspace/packages/bird-feeder/check_installed_bird_feeder.py new file mode 100644 index 000000000000..b1dc3e72facb --- /dev/null +++ b/scripts/workspaces/albatross-root-workspace/packages/bird-feeder/check_installed_bird_feeder.py @@ -0,0 +1,10 @@ +from bird_feeder import use + +try: + from albatross import fly + + raise RuntimeError("albatross installed") +except ModuleNotFoundError: + pass + +print("Success") diff --git a/scripts/workspaces/albatross-root-workspace/packages/bird-feeder/pyproject.toml b/scripts/workspaces/albatross-root-workspace/packages/bird-feeder/pyproject.toml new file mode 100644 index 000000000000..dc16cf9cb4f3 --- /dev/null +++ b/scripts/workspaces/albatross-root-workspace/packages/bird-feeder/pyproject.toml @@ -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" diff --git a/scripts/workspaces/albatross-root-workspace/packages/bird-feeder/src/bird_feeder/__init__.py b/scripts/workspaces/albatross-root-workspace/packages/bird-feeder/src/bird_feeder/__init__.py new file mode 100644 index 000000000000..856404fc346d --- /dev/null +++ b/scripts/workspaces/albatross-root-workspace/packages/bird-feeder/src/bird_feeder/__init__.py @@ -0,0 +1,5 @@ +import anyio + + +def use(): + print("squirrel") diff --git a/scripts/workspaces/albatross-root-workspace/packages/seeds/pyproject.toml b/scripts/workspaces/albatross-root-workspace/packages/seeds/pyproject.toml new file mode 100644 index 000000000000..b48f3f2d80f2 --- /dev/null +++ b/scripts/workspaces/albatross-root-workspace/packages/seeds/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "seeds" +version = "1.0.0" +dependencies = ["boltons==24.0.0"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/scripts/workspaces/albatross-root-workspace/packages/seeds/src/seeds/__init__.py b/scripts/workspaces/albatross-root-workspace/packages/seeds/src/seeds/__init__.py new file mode 100644 index 000000000000..68b303c01518 --- /dev/null +++ b/scripts/workspaces/albatross-root-workspace/packages/seeds/src/seeds/__init__.py @@ -0,0 +1,5 @@ +import boltons + + +def seeds(): + print("sunflower") diff --git a/scripts/workspaces/albatross-root-workspace/pyproject.toml b/scripts/workspaces/albatross-root-workspace/pyproject.toml new file mode 100644 index 000000000000..8dab9ce0a945 --- /dev/null +++ b/scripts/workspaces/albatross-root-workspace/pyproject.toml @@ -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" diff --git a/scripts/workspaces/albatross-root-workspace/src/albatross/__init__.py b/scripts/workspaces/albatross-root-workspace/src/albatross/__init__.py new file mode 100644 index 000000000000..1bdd1bb2ae5d --- /dev/null +++ b/scripts/workspaces/albatross-root-workspace/src/albatross/__init__.py @@ -0,0 +1,11 @@ +import tqdm +from bird_feeder import use + + +def fly(): + pass + + +if __name__ == "__main__": + print("Caw") + use() diff --git a/scripts/workspaces/albatross-virtual-workspace/packages/albatross/check_installed_albatross.py b/scripts/workspaces/albatross-virtual-workspace/packages/albatross/check_installed_albatross.py new file mode 100644 index 000000000000..e2e52c015f78 --- /dev/null +++ b/scripts/workspaces/albatross-virtual-workspace/packages/albatross/check_installed_albatross.py @@ -0,0 +1,4 @@ +from albatross import fly + +fly() +print("Success") diff --git a/scripts/workspaces/albatross-virtual-workspace/packages/albatross/pyproject.toml b/scripts/workspaces/albatross-virtual-workspace/packages/albatross/pyproject.toml new file mode 100644 index 000000000000..74fd3a1ebdaf --- /dev/null +++ b/scripts/workspaces/albatross-virtual-workspace/packages/albatross/pyproject.toml @@ -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" diff --git a/scripts/workspaces/albatross-virtual-workspace/packages/albatross/src/albatross/__init__.py b/scripts/workspaces/albatross-virtual-workspace/packages/albatross/src/albatross/__init__.py new file mode 100644 index 000000000000..1bdd1bb2ae5d --- /dev/null +++ b/scripts/workspaces/albatross-virtual-workspace/packages/albatross/src/albatross/__init__.py @@ -0,0 +1,11 @@ +import tqdm +from bird_feeder import use + + +def fly(): + pass + + +if __name__ == "__main__": + print("Caw") + use() diff --git a/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder/check_installed_bird_feeder.py b/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder/check_installed_bird_feeder.py new file mode 100644 index 000000000000..b1dc3e72facb --- /dev/null +++ b/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder/check_installed_bird_feeder.py @@ -0,0 +1,10 @@ +from bird_feeder import use + +try: + from albatross import fly + + raise RuntimeError("albatross installed") +except ModuleNotFoundError: + pass + +print("Success") diff --git a/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder/pyproject.toml b/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder/pyproject.toml new file mode 100644 index 000000000000..dc16cf9cb4f3 --- /dev/null +++ b/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder/pyproject.toml @@ -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" diff --git a/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder/src/bird_feeder/__init__.py b/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder/src/bird_feeder/__init__.py new file mode 100644 index 000000000000..856404fc346d --- /dev/null +++ b/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder/src/bird_feeder/__init__.py @@ -0,0 +1,5 @@ +import anyio + + +def use(): + print("squirrel") diff --git a/scripts/workspaces/albatross-virtual-workspace/packages/seeds/pyproject.toml b/scripts/workspaces/albatross-virtual-workspace/packages/seeds/pyproject.toml new file mode 100644 index 000000000000..b48f3f2d80f2 --- /dev/null +++ b/scripts/workspaces/albatross-virtual-workspace/packages/seeds/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "seeds" +version = "1.0.0" +dependencies = ["boltons==24.0.0"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/scripts/workspaces/albatross-virtual-workspace/packages/seeds/src/seeds/__init__.py b/scripts/workspaces/albatross-virtual-workspace/packages/seeds/src/seeds/__init__.py new file mode 100644 index 000000000000..68b303c01518 --- /dev/null +++ b/scripts/workspaces/albatross-virtual-workspace/packages/seeds/src/seeds/__init__.py @@ -0,0 +1,5 @@ +import boltons + + +def seeds(): + print("sunflower") diff --git a/scripts/workspaces/albatross-virtual-workspace/pyproject.toml b/scripts/workspaces/albatross-virtual-workspace/pyproject.toml new file mode 100644 index 000000000000..2b03125df1d4 --- /dev/null +++ b/scripts/workspaces/albatross-virtual-workspace/pyproject.toml @@ -0,0 +1,2 @@ +[tool.uv.workspace] +members = ["packages/*"]